yini-parser 1.5.0 → 1.6.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/CHANGELOG.md +33 -0
- package/README.md +183 -37
- package/dist/YINI.d.ts +22 -7
- package/dist/YINI.js +101 -0
- package/dist/core/astBuilder.d.ts +94 -15
- package/dist/core/astBuilder.js +394 -362
- package/dist/core/errorDataHandler.d.ts +6 -1
- package/dist/core/errorDataHandler.js +30 -43
- package/dist/core/internalTypes.d.ts +10 -1
- package/dist/core/objectBuilder.js +21 -6
- package/dist/core/options/defaultParserOptions.d.ts +3 -2
- package/dist/core/options/defaultParserOptions.js +2 -1
- package/dist/core/options/optionsFunctions.js +5 -1
- package/dist/core/pipeline/pipeline.js +31 -10
- package/dist/core/runtime.js +28 -19
- package/dist/grammar/generated/YiniLexer.d.ts +28 -35
- package/dist/grammar/generated/YiniLexer.js +323 -310
- package/dist/grammar/generated/YiniParser.d.ts +158 -80
- package/dist/grammar/generated/YiniParser.js +1141 -620
- package/dist/grammar/generated/YiniParserVisitor.d.ts +77 -14
- package/dist/grammar/generated/YiniParserVisitor.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -3
- package/dist/parsers/extractHeaderParts.d.ts +12 -19
- package/dist/parsers/extractHeaderParts.js +57 -46
- package/dist/parsers/parseNumber.d.ts +24 -6
- package/dist/parsers/parseNumber.js +114 -49
- package/dist/parsers/parseSectionHeader.d.ts +11 -3
- package/dist/parsers/parseSectionHeader.js +55 -43
- package/dist/parsers/parseString.js +39 -20
- package/dist/parsers/validateShebangPlacement.d.ts +3 -0
- package/dist/parsers/validateShebangPlacement.js +52 -0
- package/dist/types/index.d.ts +19 -2
- package/dist/utils/print.d.ts +1 -0
- package/dist/utils/print.js +5 -1
- package/dist/utils/string.d.ts +1 -0
- package/dist/utils/string.js +17 -1
- package/dist/utils/system.d.ts +1 -0
- package/dist/utils/system.js +6 -1
- package/dist/utils/yiniHelpers.d.ts +44 -2
- package/dist/utils/yiniHelpers.js +134 -46
- package/examples/compare-formats.md +1 -1
- package/examples/nested.yini +1 -1
- package/package.json +11 -3
- package/dist/YINI.js.map +0 -1
- package/dist/config/env.js.map +0 -1
- package/dist/core/astBuilder.js.map +0 -1
- package/dist/core/errorDataHandler.js.map +0 -1
- package/dist/core/internalTypes.js.map +0 -1
- package/dist/core/objectBuilder.js.map +0 -1
- package/dist/core/options/defaultParserOptions.js.map +0 -1
- package/dist/core/options/failLevel.js.map +0 -1
- package/dist/core/options/optionsFunctions.js.map +0 -1
- package/dist/core/parsingRules/modeFromRulesMatcher.js.map +0 -1
- package/dist/core/parsingRules/rulesConstAndGuards.js.map +0 -1
- package/dist/core/pipeline/errorListeners.js.map +0 -1
- package/dist/core/pipeline/pipeline.js.map +0 -1
- package/dist/core/resultMetadataBuilder.js.map +0 -1
- package/dist/core/runtime.js.map +0 -1
- package/dist/dev/main.d.ts +0 -1
- package/dist/dev/main.js +0 -139
- package/dist/dev/main.js.map +0 -1
- package/dist/dev/quick-test-samples/defect-inputs.d.ts +0 -37
- package/dist/dev/quick-test-samples/defect-inputs.js +0 -106
- package/dist/dev/quick-test-samples/defect-inputs.js.map +0 -1
- package/dist/dev/quick-test-samples/valid-inputs.d.ts +0 -21
- package/dist/dev/quick-test-samples/valid-inputs.js +0 -422
- package/dist/dev/quick-test-samples/valid-inputs.js.map +0 -1
- package/dist/grammar/generated/YiniLexer.js.map +0 -1
- package/dist/grammar/generated/YiniParser.js.map +0 -1
- package/dist/grammar/generated/YiniParserVisitor.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/parsers/extractHeaderParts.js.map +0 -1
- package/dist/parsers/extractSignificantYiniLine.js.map +0 -1
- package/dist/parsers/parseBoolean.js.map +0 -1
- package/dist/parsers/parseNull.js.map +0 -1
- package/dist/parsers/parseNumber.js.map +0 -1
- package/dist/parsers/parseSectionHeader.js.map +0 -1
- package/dist/parsers/parseString.js.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/utils/number.js.map +0 -1
- package/dist/utils/object.js.map +0 -1
- package/dist/utils/pathAndFileName.js.map +0 -1
- package/dist/utils/print.js.map +0 -1
- package/dist/utils/string.js.map +0 -1
- package/dist/utils/system.js.map +0 -1
- package/dist/utils/yiniHelpers.js.map +0 -1
package/dist/core/astBuilder.js
CHANGED
|
@@ -36,13 +36,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
/**
|
|
40
|
+
* Here is where we walk the parse tree (CST) and build/validate the AST.
|
|
41
|
+
*/
|
|
42
|
+
// src/core/astBuilder.ts
|
|
39
43
|
const env_1 = require("../config/env");
|
|
40
44
|
const YiniParserVisitor_1 = __importDefault(require("../grammar/generated/YiniParserVisitor"));
|
|
41
45
|
const extractSignificantYiniLine_1 = require("../parsers/extractSignificantYiniLine");
|
|
42
46
|
const parseBoolean_1 = __importDefault(require("../parsers/parseBoolean"));
|
|
43
47
|
const parseNull_1 = __importDefault(require("../parsers/parseNull"));
|
|
44
48
|
const parseNumber_1 = __importDefault(require("../parsers/parseNumber"));
|
|
45
|
-
// import parseNumber from '../parsers/parseNumber'
|
|
46
49
|
const parseSectionHeader_1 = __importDefault(require("../parsers/parseSectionHeader"));
|
|
47
50
|
const parseString_1 = __importStar(require("../parsers/parseString"));
|
|
48
51
|
const number_1 = require("../utils/number");
|
|
@@ -93,54 +96,9 @@ const makeListValue = (elems = [], tag = undefined) => {
|
|
|
93
96
|
const makeObjectValue = (entries = {}, tag = undefined) => {
|
|
94
97
|
return { type: 'Object', entries, tag };
|
|
95
98
|
};
|
|
96
|
-
function trimQuotes(text) {
|
|
97
|
-
// STRING token already excludes quotes; the rule returns the literal with quotes present.
|
|
98
|
-
// We’ll reliably strip the outer quote(s) and leave contents as-is (concat pieces handled below).
|
|
99
|
-
const q = text[0];
|
|
100
|
-
if ((q === '"' || q === "'") &&
|
|
101
|
-
text.length >= 2 &&
|
|
102
|
-
text[text.length - 1] === q) {
|
|
103
|
-
return text.slice(1, -1);
|
|
104
|
-
}
|
|
105
|
-
// Triple-quoted cases are handled by the lexer too; same stripping works since token text begins with quotes.
|
|
106
|
-
if (text.startsWith('"""') && text.endsWith('"""') && text.length >= 6) {
|
|
107
|
-
return text.slice(3, -3);
|
|
108
|
-
}
|
|
109
|
-
return text;
|
|
110
|
-
}
|
|
111
99
|
function makeSection(name, level) {
|
|
112
100
|
return { sectionName: name, level, members: new Map(), children: [] };
|
|
113
101
|
}
|
|
114
|
-
/** Parse SECTION_HEAD token text → {level, name}.
|
|
115
|
-
* Supports repeated markers (^^^^) and shorthand (^7) (Spec 5.2–5.3.1). :contentReference[oaicite:5]{index=5}:contentReference[oaicite:6]{index=6}
|
|
116
|
-
*/
|
|
117
|
-
// function parseSectionHeadToken(raw: string): { level: number; name: string } {
|
|
118
|
-
// // SECTION_HEAD token text includes: optional WS, marker(s) or shorthand, WS, IDENT (possibly backticked), NL+
|
|
119
|
-
// // We only need the visible line content up to NL.
|
|
120
|
-
// const line = raw.split(/\r?\n/)[0]
|
|
121
|
-
// // Extract marker block and name
|
|
122
|
-
// // Examples: "^^ Section", "^7 `Section name`", "< MySection"
|
|
123
|
-
// const m = line.match(/^\s*([\^<§€]+|\^|\<|§|€)(\d+)?[ \t]+(.+?)\s*$/)
|
|
124
|
-
// if (m) {
|
|
125
|
-
// const markerRun = m[1]
|
|
126
|
-
// const numeric = m[2]
|
|
127
|
-
// let level: number
|
|
128
|
-
// if (numeric) {
|
|
129
|
-
// level = parseInt(numeric, 10)
|
|
130
|
-
// } else {
|
|
131
|
-
// // count repeated marker chars (^^^^)
|
|
132
|
-
// level = markerRun.length
|
|
133
|
-
// }
|
|
134
|
-
// // Section name may be backticked: `Name with spaces`
|
|
135
|
-
// let name = m[3]
|
|
136
|
-
// if (name.startsWith('`') && name.endsWith('`')) {
|
|
137
|
-
// name = name.slice(1, -1)
|
|
138
|
-
// }
|
|
139
|
-
// return { level, name }
|
|
140
|
-
// }
|
|
141
|
-
// // Fallback: be defensive
|
|
142
|
-
// return { level: 1, name: line.trim() }
|
|
143
|
-
// }
|
|
144
102
|
// --- Builder Visitor -----------------------------------------------------
|
|
145
103
|
/**
|
|
146
104
|
* This interface defines a complete generic visitor for a parse tree produced
|
|
@@ -150,6 +108,7 @@ function makeSection(name, level) {
|
|
|
150
108
|
* operations with no return type.
|
|
151
109
|
*/
|
|
152
110
|
// export default class YINIVisitor<IResult> extends YiniParserVisitor<IResult> {
|
|
111
|
+
// export default class ASTBuilder<Result> extends YiniParserVisitor<Result> {
|
|
153
112
|
class ASTBuilder extends YiniParserVisitor_1.default {
|
|
154
113
|
/**
|
|
155
114
|
* @param metaFileName If parsing from a file, provide the file name here so the meta information can be updated accordingly.
|
|
@@ -158,6 +117,7 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
158
117
|
constructor(errorHandler, options, sourceType, metaFileName) {
|
|
159
118
|
super();
|
|
160
119
|
this.errorHandler = null;
|
|
120
|
+
this.ignoredSectionLevel = null;
|
|
161
121
|
this.meta_hasYiniMarker = false; // For stats.
|
|
162
122
|
// private meta_numOfSections = 0 // For stats.
|
|
163
123
|
this._numOfMembers = 0; // For error checking and stats.
|
|
@@ -175,8 +135,6 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
175
135
|
* @param ctx the parse tree
|
|
176
136
|
* @return the visitor result
|
|
177
137
|
*/
|
|
178
|
-
// visitYini?: (ctx: YiniContext) => Result
|
|
179
|
-
// visitYini?: (ctx: YiniContext) => any
|
|
180
138
|
this.visitYini = (ctx) => {
|
|
181
139
|
// children: prolog?, stmt*, terminal?, EOF
|
|
182
140
|
ctx.children?.forEach((c) => this.visit?.(c));
|
|
@@ -197,36 +155,63 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
197
155
|
* @param ctx the parse tree
|
|
198
156
|
* @return the visitor result
|
|
199
157
|
*/
|
|
200
|
-
// visitTerminal_stmt?: (ctx: Terminal_stmtContext) => Result;
|
|
201
158
|
this.visitTerminal_stmt = (ctx) => {
|
|
202
159
|
(0, print_1.debugPrint)('-> Entered visitTerminal_stmt(..)');
|
|
203
160
|
let rawText = ctx.getText().trim();
|
|
204
161
|
(0, print_1.debugPrint)('rawText = "' + rawText + '"');
|
|
205
|
-
// rawText = extractYiniLine(rawText) // Remove possible comments.
|
|
206
|
-
// rawText = stripCommentsAndAfter(rawText.split('\n', 1)[0]).trim() // Remove possible comments.
|
|
207
162
|
rawText = (0, yiniHelpers_1.stripCommentsAndAfter)(rawText); // Remove possible comments.
|
|
208
163
|
(0, print_1.debugPrint)('rawText2 = "' + rawText + '"');
|
|
209
164
|
if (rawText.toLowerCase() === '/end') {
|
|
210
165
|
// NOTE: Below, maybe not reached at all.
|
|
211
166
|
if (this.ast.terminatorSeen) {
|
|
212
167
|
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
213
|
-
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Warning', 'Hit a duplicate terminator in document', `'${rawText}' already exists in this file
|
|
168
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Warning', 'Hit a duplicate terminator in document', `'${rawText}' already exists in this file. A YINI document may contain only one document terminator ('/END'). The terminator is optional in lenient mode and required in strict mode unless parser options explicitly override that policy.`);
|
|
214
169
|
}
|
|
215
170
|
}
|
|
216
171
|
else {
|
|
217
172
|
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
218
|
-
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Encountered
|
|
173
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Encountered unknown syntax for terminator', `Got '${rawText}', but expected '/END' (case insensitive).`);
|
|
219
174
|
}
|
|
220
175
|
this.ast.terminatorSeen = true;
|
|
221
176
|
return null;
|
|
222
177
|
};
|
|
178
|
+
/**
|
|
179
|
+
* Visit a parse tree produced by `YiniParser.terminal_trivia`.
|
|
180
|
+
* @param ctx the parse tree
|
|
181
|
+
* @return the visitor result
|
|
182
|
+
*/
|
|
183
|
+
this.visitTerminal_trivia = (ctx) => {
|
|
184
|
+
ctx.children?.forEach((c) => this.visit?.(c));
|
|
185
|
+
return null;
|
|
186
|
+
};
|
|
223
187
|
/**
|
|
224
188
|
* Visit a parse tree produced by `YiniParser.stmt`.
|
|
225
189
|
* @param ctx the parse tree
|
|
226
|
-
* @grammarRule eol | SECTION_HEAD | assignment | colon_list_decl | marker_stmt | bad_member
|
|
227
190
|
* @return the visitor result
|
|
228
191
|
*/
|
|
229
192
|
// visitStmt?: (ctx: StmtContext) => Result
|
|
193
|
+
/**
|
|
194
|
+
* Visit a parse tree produced by `YiniParser.full_line_comment_stmt`.
|
|
195
|
+
* @param ctx the parse tree
|
|
196
|
+
* @return the visitor result
|
|
197
|
+
*/
|
|
198
|
+
this.visitFull_line_comment_stmt = (_ctx) => {
|
|
199
|
+
return null;
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* Visit a parse tree produced by `YiniParser.disabled_line_stmt`.
|
|
203
|
+
* @param ctx the parse tree
|
|
204
|
+
* @return the visitor result
|
|
205
|
+
*/
|
|
206
|
+
this.visitDisabled_line_stmt = (_ctx) => {
|
|
207
|
+
return null;
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* Visit a parse tree produced by `YiniParser.stmt`.
|
|
211
|
+
* @param ctx the parse tree
|
|
212
|
+
* @grammarRule eol | full_line_comment_stmt | disabled_line_stmt | SECTION_HEAD | invalid_section_stmt | assignment | meta_stmt | bad_member
|
|
213
|
+
* @return the visitor result
|
|
214
|
+
*/
|
|
230
215
|
this.visitStmt = (ctx) => {
|
|
231
216
|
const child = ctx.getChild(0);
|
|
232
217
|
const ruleName = child?.constructor?.name ?? '';
|
|
@@ -237,6 +222,15 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
237
222
|
// }
|
|
238
223
|
if (ruleName.includes('EolContext'))
|
|
239
224
|
return this.visitEol?.(child);
|
|
225
|
+
if (ruleName.includes('Full_line_comment_stmtContext'))
|
|
226
|
+
return this.visitFull_line_comment_stmt?.(child);
|
|
227
|
+
if (ruleName.includes('Disabled_line_stmtContext'))
|
|
228
|
+
return this.visitDisabled_line_stmt?.(child);
|
|
229
|
+
if (this.isIgnoringDuplicateSection() && !ctx.SECTION_HEAD()) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
if (ruleName.includes('Invalid_section_stmtContext'))
|
|
233
|
+
return this.visitInvalid_section_stmt?.(child);
|
|
240
234
|
if (ruleName.includes('AssignmentContext'))
|
|
241
235
|
return this.visitAssignment?.(child);
|
|
242
236
|
if (ruleName.includes('Meta_stmtContext'))
|
|
@@ -249,6 +243,9 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
249
243
|
(0, print_1.debugPrint)('S3, header: >>>' + header + '<<<');
|
|
250
244
|
if (!!header) {
|
|
251
245
|
const { sectionName, sectionLevel } = (0, parseSectionHeader_1.default)(header, this.errorHandler, ctx);
|
|
246
|
+
if (this.shouldSkipSectionAtLevel(sectionLevel)) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
252
249
|
// Validate level sequencing per spec 5.3 (no skipping upward)
|
|
253
250
|
const currentLevel = this.sectionStack[this.sectionStack.length - 1].level;
|
|
254
251
|
if (sectionLevel > currentLevel + 1) {
|
|
@@ -262,6 +259,10 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
262
259
|
// bad_member fallback
|
|
263
260
|
return this.visitBad_member?.(ctx.getChild(0));
|
|
264
261
|
};
|
|
262
|
+
this.visitInvalid_section_stmt = (ctx) => {
|
|
263
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Invalid section header', `Offending section header: '${ctx.getText().trim()}'`, 'Section headers must use a valid marker sequence followed by a valid section name.');
|
|
264
|
+
return null;
|
|
265
|
+
};
|
|
265
266
|
/**
|
|
266
267
|
* Visit a parse tree produced by `YiniParser.meta_stmt`.
|
|
267
268
|
* @param ctx the parse tree
|
|
@@ -286,31 +287,19 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
286
287
|
*/
|
|
287
288
|
this.visitDirective = (ctx) => {
|
|
288
289
|
(0, print_1.debugPrint)('-> Entered visitDirective(..)');
|
|
289
|
-
|
|
290
|
-
(0, print_1.debugPrint)('rawText = "' + rawText + '"');
|
|
291
|
-
// NOTE: Important to strip any possible comments on the same line.
|
|
292
|
-
rawText = (0, yiniHelpers_1.stripCommentsAndAfter)(rawText); // Remove possible comments.
|
|
293
|
-
(0, print_1.debugPrint)('rawText2 = "' + rawText + '"');
|
|
290
|
+
const rawText = (0, yiniHelpers_1.stripCommentsAndAfter)(ctx.getText().trim());
|
|
294
291
|
if (this.mapSectionNamePaths.size || this._numOfMembers) {
|
|
295
|
-
|
|
296
|
-
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), this.isStrict ? 'Syntax-Error' : 'Syntax-Warning', `Found a directive statement in the wrong place ${this.isStrict ? '(strict mode)' : '(lenient mode)'}`, `Directive '${rawText}' must appear only at the beginning of the document, before any sections or members.`, `Tip: Move the line with '${rawText}' to the very top of the file (but after a possible #! line or comments).`);
|
|
292
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), this.isStrict ? 'Syntax-Error' : 'Syntax-Warning', `Found a directive statement in the wrong place ${this.isStrict ? '(strict mode)' : '(lenient mode)'}`, `Directive '${rawText}' must appear only at the beginning of the document, before any sections or members.`, 'Move the directive to the top of the file, after a possible shebang, comments, or whitespace.');
|
|
297
293
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
this.
|
|
301
|
-
}
|
|
302
|
-
else if (rawText.toLowerCase() === '@yini') {
|
|
303
|
-
if (this.ast.yiniMarkerSeen) {
|
|
304
|
-
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
305
|
-
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), this.isStrict ? 'Syntax-Error' : 'Syntax-Warning', `Hit a duplicate YINI Marker in document`, `'${rawText}' already exists in this file, it's enough with only one YINI Marker ('@YINI').`);
|
|
306
|
-
}
|
|
294
|
+
const yiniDirective = ctx.yini_directive?.();
|
|
295
|
+
if (yiniDirective) {
|
|
296
|
+
return this.visitYini_directive(yiniDirective);
|
|
307
297
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
298
|
+
if (ctx.INCLUDE_TOKEN?.()) {
|
|
299
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', "Unsupported directive '@include'", "'@include' is reserved for possible future use, but is not currently supported by this parser.", 'Remove the directive or preprocess included files before parsing.');
|
|
300
|
+
return null;
|
|
311
301
|
}
|
|
312
|
-
|
|
313
|
-
this.ast.yiniMarkerSeen = true;
|
|
302
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Encountered unknown directive statement', `Got '${rawText}'.`, "Expected '@yini', '@yini strict', '@yini lenient', or a supported directive.");
|
|
314
303
|
return null;
|
|
315
304
|
};
|
|
316
305
|
/**
|
|
@@ -406,10 +395,6 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
406
395
|
*/
|
|
407
396
|
let valueContext = ctx.value?.();
|
|
408
397
|
let valueNode;
|
|
409
|
-
const hasEquals = rawMemberText.includes('=');
|
|
410
|
-
// const hasTextAfterEquals = hasEquals
|
|
411
|
-
// ? rawMemberText.split('=').slice(1).join('=').trim().length > 0
|
|
412
|
-
// : false
|
|
413
398
|
const eqIndex = rawMemberText.indexOf('=');
|
|
414
399
|
const hasTextAfterEquals = eqIndex >= 0 && rawMemberText.slice(eqIndex + 1).trim().length > 0;
|
|
415
400
|
if (!valueContext || !rawValue) {
|
|
@@ -441,27 +426,6 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
441
426
|
if ((0, env_1.isDebug)()) {
|
|
442
427
|
(0, print_1.printObject)(valueNode);
|
|
443
428
|
}
|
|
444
|
-
/*
|
|
445
|
-
if (!valueNode) {
|
|
446
|
-
this.errorHandler!.pushOrBail(
|
|
447
|
-
toErrorLocation(ctx),
|
|
448
|
-
'Syntax-Error',
|
|
449
|
-
'Invalid value',
|
|
450
|
-
`Invalid value for key '${resultKey}' in member (<key> = <value> pair).`,
|
|
451
|
-
`Got '${rawValue}', but expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`,
|
|
452
|
-
)
|
|
453
|
-
} else if (
|
|
454
|
-
valueNode.type === 'Undefined' &&
|
|
455
|
-
valueNode.tag !== 'Invalid string literal already reported'
|
|
456
|
-
) {
|
|
457
|
-
this.errorHandler!.pushOrBail(
|
|
458
|
-
toErrorLocation(ctx),
|
|
459
|
-
'Syntax-Error',
|
|
460
|
-
'Invalid value',
|
|
461
|
-
`Invalid value for key '${resultKey}' in member (<key> = <value> pair).`,
|
|
462
|
-
`Got '${rawValue}', but expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`,
|
|
463
|
-
)
|
|
464
|
-
}*/
|
|
465
429
|
if (!valueNode) {
|
|
466
430
|
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
467
431
|
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Invalid value', `Invalid value for key '${resultKey}' in member (<key> = <value> pair).`, `Got '${rawValue}', but expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`);
|
|
@@ -490,26 +454,17 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
490
454
|
* @param ctx the parse tree
|
|
491
455
|
* @return the visitor result
|
|
492
456
|
*/
|
|
493
|
-
// visitValue?: (ctx: ValueContext) => Result
|
|
494
457
|
this.visitValue = (ctx) => {
|
|
495
458
|
(0, print_1.debugPrint)('----------------------------');
|
|
496
459
|
(0, print_1.debugPrint)('-> Entered visitValue(..)');
|
|
497
460
|
let valueNode = undefined;
|
|
498
|
-
if (ctx.
|
|
499
|
-
(0, print_1.debugPrint)(' visiting
|
|
500
|
-
valueNode = this.
|
|
501
|
-
}
|
|
502
|
-
else if (ctx.string_literal()) {
|
|
503
|
-
(0, print_1.debugPrint)(' visiting visitString_literal(..)');
|
|
504
|
-
valueNode = this.visitString_literal(ctx.string_literal());
|
|
505
|
-
}
|
|
506
|
-
else if (ctx.number_literal()) {
|
|
507
|
-
(0, print_1.debugPrint)(' visiting visitNumber_literal(..)');
|
|
508
|
-
valueNode = this.visitNumber_literal(ctx.number_literal());
|
|
461
|
+
if (ctx.concat_expression()) {
|
|
462
|
+
(0, print_1.debugPrint)(' visiting visitConcat_expression(..)');
|
|
463
|
+
valueNode = this.visitConcat_expression(ctx.concat_expression());
|
|
509
464
|
}
|
|
510
|
-
else if (ctx.
|
|
511
|
-
(0, print_1.debugPrint)(' visiting
|
|
512
|
-
valueNode = this.
|
|
465
|
+
else if (ctx.scalar_value()) {
|
|
466
|
+
(0, print_1.debugPrint)(' visiting visitScalar_value(..)');
|
|
467
|
+
valueNode = this.visitScalar_value(ctx.scalar_value());
|
|
513
468
|
}
|
|
514
469
|
else if (ctx.list_literal()) {
|
|
515
470
|
(0, print_1.debugPrint)(' visiting visitList_literal(..)');
|
|
@@ -531,109 +486,164 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
531
486
|
return valueNode;
|
|
532
487
|
};
|
|
533
488
|
/**
|
|
534
|
-
* Visit a parse tree produced by `YiniParser.
|
|
489
|
+
* Visit a parse tree produced by `YiniParser.scalar_value`.
|
|
535
490
|
* @param ctx the parse tree
|
|
536
491
|
* @return the visitor result
|
|
537
492
|
*/
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
const pieces = [
|
|
543
|
-
ctx.STRING(),
|
|
544
|
-
...(ctx.string_concat_list()?.map((c) => c.STRING()) ?? []),
|
|
545
|
-
]
|
|
546
|
-
|
|
547
|
-
try {
|
|
548
|
-
for (const token of pieces) {
|
|
549
|
-
const tokenText = token.getText()
|
|
550
|
-
const parsed = this.extractStringKindAndValue(tokenText)
|
|
551
|
-
|
|
552
|
-
try {
|
|
553
|
-
text += parseStringLiteral(parsed)
|
|
554
|
-
} catch (err: unknown) {
|
|
555
|
-
const msg = '' + (<any>err)?.message
|
|
556
|
-
this.errorHandler!.pushOrBail(
|
|
557
|
-
toErrorLocation(ctx),
|
|
558
|
-
'Syntax-Error',
|
|
559
|
-
'Parse error in string',
|
|
560
|
-
`${msg}`,
|
|
561
|
-
)
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
return makeScalarValue('String', text)
|
|
566
|
-
} catch (err: unknown) {
|
|
567
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
568
|
-
|
|
569
|
-
let msgWhat = 'Invalid string literal'
|
|
570
|
-
let msgWhy = msg
|
|
571
|
-
let msgHint = ''
|
|
572
|
-
|
|
573
|
-
if (err instanceof CYiniStringParseError) {
|
|
574
|
-
msgWhat = 'Invalid escape sequence in string'
|
|
575
|
-
msgWhy = msg
|
|
576
|
-
|
|
577
|
-
if (/Invalid escape sequence \\\\/.test(msg)) {
|
|
578
|
-
msgHint =
|
|
579
|
-
'Use double backslashes (\\\\) in C-strings, or use a raw string for file paths.'
|
|
580
|
-
} else if (/end of string/i.test(msg)) {
|
|
581
|
-
msgHint =
|
|
582
|
-
'Check that all escape sequences in the C-string are complete and valid.'
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
this.errorHandler!.pushOrBail(
|
|
587
|
-
toErrorLocation(ctx),
|
|
588
|
-
'Syntax-Error',
|
|
589
|
-
msgWhat,
|
|
590
|
-
msgWhy,
|
|
591
|
-
msgHint,
|
|
592
|
-
)
|
|
593
|
-
|
|
594
|
-
return makeScalarValue(
|
|
595
|
-
'Undefined',
|
|
596
|
-
undefined,
|
|
597
|
-
'Invalid string literal',
|
|
598
|
-
)
|
|
493
|
+
this.visitScalar_value = (ctx) => {
|
|
494
|
+
(0, print_1.debugPrint)('-> Entered visitScalar_value(..)');
|
|
495
|
+
if (ctx.string_literal()) {
|
|
496
|
+
return this.visitString_literal(ctx.string_literal());
|
|
599
497
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
498
|
+
if (ctx.number_literal()) {
|
|
499
|
+
return this.visitNumber_literal(ctx.number_literal());
|
|
500
|
+
}
|
|
501
|
+
if (ctx.boolean_literal()) {
|
|
502
|
+
return this.visitBoolean_literal(ctx.boolean_literal());
|
|
503
|
+
}
|
|
504
|
+
if (ctx.null_literal()) {
|
|
505
|
+
return this.visitNull_literal(ctx.null_literal());
|
|
506
|
+
}
|
|
507
|
+
return makeScalarValue('Undefined', undefined, 'Invalid scalar value');
|
|
508
|
+
};
|
|
509
|
+
/**
|
|
510
|
+
* Visit a parse tree produced by `YiniParser.concat_expression`.
|
|
511
|
+
*
|
|
512
|
+
* Grammar:
|
|
513
|
+
* concat_expression : STRING concat_tail+ ;
|
|
514
|
+
*
|
|
515
|
+
* Spec:
|
|
516
|
+
* - Concatenation must begin with a string literal.
|
|
517
|
+
* - In strict mode, every operand must be a string literal.
|
|
518
|
+
* - In lenient mode, later operands may be string, number, boolean, or null.
|
|
519
|
+
* - YINI does not define numeric addition.
|
|
520
|
+
*
|
|
521
|
+
* Examples:
|
|
522
|
+
* "Port: " + 8080 // lenient valid
|
|
523
|
+
* "1" + 2 + 3 // lenient valid
|
|
524
|
+
* 8080 + " is the port" // invalid: must begin with string literal
|
|
525
|
+
* 1 + 2 + "3" // invalid: must begin with string literal
|
|
526
|
+
* 1 + 2 + 3 // invalid: YINI does not define numeric addition
|
|
527
|
+
*/
|
|
528
|
+
this.visitConcat_expression = (ctx) => {
|
|
529
|
+
(0, print_1.debugPrint)('-> Entered visitConcat_expression(..)');
|
|
530
|
+
const operands = [];
|
|
531
|
+
const firstStringToken = ctx.STRING();
|
|
532
|
+
if (!firstStringToken) {
|
|
533
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Invalid concatenation expression', 'A concatenation expression must begin with a string literal.', 'Start the expression with a quoted string, for example: "value: " + 123.');
|
|
534
|
+
return makeScalarValue('Undefined', undefined, 'Invalid concatenation start already reported');
|
|
535
|
+
}
|
|
536
|
+
operands.push(this.parseStringToken(firstStringToken.getText(), ctx));
|
|
537
|
+
for (const tail of ctx.concat_tail_list()) {
|
|
538
|
+
const operand = this.visitConcat_tail(tail);
|
|
539
|
+
operands.push(operand);
|
|
540
|
+
}
|
|
541
|
+
if (operands.some((operand) => !operand || operand.type === 'Undefined')) {
|
|
542
|
+
return makeScalarValue('Undefined', undefined, 'Invalid concatenation operand already reported');
|
|
543
|
+
}
|
|
544
|
+
if (this.isStrict) {
|
|
545
|
+
const firstNonString = operands.find((operand) => operand.type !== 'String');
|
|
546
|
+
if (firstNonString) {
|
|
547
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Invalid strict-mode concatenation expression', 'In strict mode, all concatenation operands must be string literals.', 'Convert non-string operands to explicit string literals.');
|
|
548
|
+
return makeScalarValue('Undefined', undefined, 'Invalid strict concatenation already reported');
|
|
613
549
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
550
|
+
}
|
|
551
|
+
const result = operands
|
|
552
|
+
.map((operand) => this.stringifyConcatOperand(operand))
|
|
553
|
+
.join('');
|
|
554
|
+
return makeScalarValue('String', result, 'Concatenated string');
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* Visit a parse tree produced by `YiniParser.concat_tail`.
|
|
558
|
+
* @param ctx the parse tree
|
|
559
|
+
* @return the visitor result
|
|
560
|
+
*/
|
|
561
|
+
this.visitConcat_tail = (ctx) => {
|
|
562
|
+
(0, print_1.debugPrint)('-> Entered visitConcat_tail(..)');
|
|
563
|
+
const operandCtx = ctx.concat_operand();
|
|
564
|
+
if (!operandCtx) {
|
|
565
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Missing concatenation operand', 'Expected a value after the + operator.', 'Add a string literal, number, boolean, or null after +.');
|
|
566
|
+
return makeScalarValue('Undefined', undefined, 'Missing concatenation operand already reported');
|
|
567
|
+
}
|
|
568
|
+
return this.visitConcat_operand(operandCtx);
|
|
569
|
+
};
|
|
570
|
+
/**
|
|
571
|
+
* Visit a parse tree produced by `YiniParser.concat_operand`.
|
|
572
|
+
*
|
|
573
|
+
* @note Must use token accessors, not literal visitors.
|
|
574
|
+
* Use STRING() instead of string_literal(), etc.
|
|
575
|
+
*
|
|
576
|
+
* @param ctx the parse tree
|
|
577
|
+
* @return the visitor result
|
|
578
|
+
*/
|
|
579
|
+
this.visitConcat_operand = (ctx) => {
|
|
580
|
+
(0, print_1.debugPrint)('-> Entered visitConcat_operand(..)');
|
|
581
|
+
if (ctx.STRING()) {
|
|
582
|
+
return this.parseStringToken(ctx.STRING().getText(), ctx);
|
|
583
|
+
}
|
|
584
|
+
if (ctx.NUMBER()) {
|
|
585
|
+
const parsedNum = (0, parseNumber_1.default)(ctx.NUMBER().getText());
|
|
586
|
+
if (parsedNum?.value !== 0 &&
|
|
587
|
+
(!parsedNum?.value ||
|
|
588
|
+
(0, number_1.isNaNValue)(parsedNum.value) ||
|
|
589
|
+
(0, number_1.isInfinityValue)(parsedNum.value))) {
|
|
590
|
+
return makeScalarValue('Undefined', undefined, parsedNum?.tag ?? 'Invalid number literal');
|
|
634
591
|
}
|
|
592
|
+
return makeScalarValue('Number', parsedNum.value, parsedNum.tag);
|
|
593
|
+
}
|
|
594
|
+
if (ctx.BOOLEAN_TRUE() || ctx.BOOLEAN_FALSE()) {
|
|
595
|
+
return makeScalarValue('Boolean', (0, parseBoolean_1.default)(ctx.getText()));
|
|
635
596
|
}
|
|
636
|
-
|
|
597
|
+
if (ctx.NULL()) {
|
|
598
|
+
return makeScalarValue('Null', null, 'Explicit Null');
|
|
599
|
+
}
|
|
600
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Invalid concatenation operand', `Got '${ctx.getText()}', but expected a string literal, number literal, boolean literal, or null literal.`, 'Lists and inline objects cannot be used as concatenation operands.');
|
|
601
|
+
return makeScalarValue('Undefined', undefined, 'Invalid concatenation operand already reported');
|
|
602
|
+
};
|
|
603
|
+
/*
|
|
604
|
+
* Visit a parse tree produced by `YiniParser.visitString_literal`.
|
|
605
|
+
* @note Should parse exactly one string literal. No concatenation logic here.
|
|
606
|
+
*
|
|
607
|
+
* @param ctx the parse tree
|
|
608
|
+
* @return the visitor result
|
|
609
|
+
*/
|
|
610
|
+
this.visitString_literal = (ctx) => {
|
|
611
|
+
// const rawText = ctx.getText()
|
|
612
|
+
// const parsed = this.extractStringKindAndValue(rawText)
|
|
613
|
+
// try {
|
|
614
|
+
// const value = parseStringLiteral(parsed)
|
|
615
|
+
// return makeScalarValue('String', value)
|
|
616
|
+
// } catch (err: unknown) {
|
|
617
|
+
// const msg = err instanceof Error ? err.message : String(err)
|
|
618
|
+
// let msgWhat = 'Parse error in string'
|
|
619
|
+
// let msgWhy = msg
|
|
620
|
+
// let msgHint = ''
|
|
621
|
+
// if (err instanceof CYiniStringParseError) {
|
|
622
|
+
// if (/Invalid escape sequence/i.test(msg)) {
|
|
623
|
+
// msgWhat = 'Invalid escape sequence in string'
|
|
624
|
+
// msgHint =
|
|
625
|
+
// 'Use double backslashes (\\\\) in C-strings, or use a raw string if escapes are not needed.'
|
|
626
|
+
// } else if (/end of string/i.test(msg)) {
|
|
627
|
+
// msgWhat = 'Incomplete escape sequence in string'
|
|
628
|
+
// msgHint =
|
|
629
|
+
// 'Check that all escape sequences in the C-string are complete and valid.'
|
|
630
|
+
// }
|
|
631
|
+
// }
|
|
632
|
+
// this.errorHandler!.pushOrBail(
|
|
633
|
+
// toErrorLocation(ctx),
|
|
634
|
+
// 'Syntax-Error',
|
|
635
|
+
// msgWhat,
|
|
636
|
+
// msgWhy,
|
|
637
|
+
// msgHint,
|
|
638
|
+
// )
|
|
639
|
+
// return makeScalarValue(
|
|
640
|
+
// 'Undefined',
|
|
641
|
+
// undefined,
|
|
642
|
+
// 'Invalid string literal already reported',
|
|
643
|
+
// )
|
|
644
|
+
// }
|
|
645
|
+
// }
|
|
646
|
+
return this.parseStringToken(ctx.STRING().getText(), ctx);
|
|
637
647
|
};
|
|
638
648
|
/**
|
|
639
649
|
* Visit a parse tree produced by `YiniParser.number_literal`.
|
|
@@ -712,8 +722,7 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
712
722
|
// visitList_literal?: (ctx: List_literalContext) => Result
|
|
713
723
|
this.visitList_literal = (ctx) => {
|
|
714
724
|
(0, print_1.debugPrint)('-> Entered visitList_literal(..)');
|
|
715
|
-
|
|
716
|
-
const elems = this.visitElements(ctx.elements());
|
|
725
|
+
const elems = ctx.elements() ? this.visitElements(ctx.elements()) : [];
|
|
717
726
|
const value = makeListValue(elems, 'From bracketed list');
|
|
718
727
|
(0, print_1.debugPrint)('<- About to exit visitList_literal(..)...');
|
|
719
728
|
if ((0, env_1.isDebug)()) {
|
|
@@ -731,12 +740,14 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
731
740
|
this.visitElements = (ctx) => {
|
|
732
741
|
(0, print_1.debugPrint)('-> Entered visitElements(..)');
|
|
733
742
|
(0, print_1.debugPrint)(' elements.length = ' + ctx?.value_list().length);
|
|
743
|
+
if (this.isStrict && this.hasTrailingComma(ctx)) {
|
|
744
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Trailing comma is not allowed in strict mode', 'A list literal ended with a trailing comma.', 'Remove the final comma, or parse the document in lenient mode.');
|
|
745
|
+
}
|
|
734
746
|
const elems = !ctx?.value_list()
|
|
735
747
|
? []
|
|
736
748
|
: ctx.value_list().map((elem) => {
|
|
737
749
|
const valueNode = this.visitValue(elem);
|
|
738
750
|
if (!valueNode) {
|
|
739
|
-
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
740
751
|
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Invalid list element', `Invalid list element: '${elem?.getText()}'`, `Expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`);
|
|
741
752
|
}
|
|
742
753
|
return valueNode;
|
|
@@ -756,10 +767,6 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
756
767
|
*/
|
|
757
768
|
this.visitObject_literal = (ctx) => {
|
|
758
769
|
(0, print_1.debugPrint)('-> Entered visitObject_literal(..)');
|
|
759
|
-
// debugPrint('entries.EMPTY_OBJECT = ' + ctx?.EMPTY_OBJECT())
|
|
760
|
-
// debugPrint('entries.length = ' + ctx?.object_members())
|
|
761
|
-
// printObject(ctx)
|
|
762
|
-
// const entries = this.visitObject_members(ctx?.object_members())
|
|
763
770
|
const entries = ctx.object_members()
|
|
764
771
|
? this.visitObject_members(ctx.object_members())
|
|
765
772
|
: {};
|
|
@@ -776,114 +783,127 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
776
783
|
* @param ctx the parse tree
|
|
777
784
|
* @grammarRule object_member (COMMA NL* object_member)* COMMA?
|
|
778
785
|
* @return the visitor result
|
|
786
|
+
*
|
|
787
|
+
* @note
|
|
788
|
+
* SPEC says:
|
|
789
|
+
* - Lenient: first wins + warning
|
|
790
|
+
* - Strict: error
|
|
791
|
+
*
|
|
792
|
+
* NEVER silently overwrite.
|
|
779
793
|
*/
|
|
780
794
|
this.visitObject_members = (ctx) => {
|
|
781
795
|
(0, print_1.debugPrint)('-> Entered visitObject_members(..)');
|
|
782
|
-
|
|
783
|
-
|
|
796
|
+
if (this.isStrict && this.hasTrailingComma(ctx)) {
|
|
797
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Trailing comma is not allowed in strict mode', 'An inline object literal ended with a trailing comma.', 'Remove the final comma, or parse the document in lenient mode.');
|
|
798
|
+
}
|
|
784
799
|
const entries = {};
|
|
785
800
|
ctx.object_member_list().forEach((member) => {
|
|
786
801
|
const { key, value } = this.visitObject_member(member);
|
|
787
|
-
(
|
|
802
|
+
if (!value || value.type === 'Undefined') {
|
|
803
|
+
(0, print_1.debugPrint)('Skip inserting Undefined');
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (Object.prototype.hasOwnProperty.call(entries, key)) {
|
|
807
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(member), this.isStrict ? 'Syntax-Error' : 'Syntax-Warning', 'Duplicate inline object member', `Object member '${key}' is already defined in this inline object.`, this.isStrict
|
|
808
|
+
? 'Inline object member keys must be unique in strict mode.'
|
|
809
|
+
: 'The first object member value is kept; later duplicates are ignored.');
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
788
812
|
entries[key] = value;
|
|
789
813
|
});
|
|
790
|
-
(0, print_1.debugPrint)('<- About to exit visitObject_members(..)');
|
|
791
814
|
return entries;
|
|
792
815
|
};
|
|
793
816
|
/**
|
|
794
817
|
* Visit a parse tree produced by `YiniParser.object_member`.
|
|
795
818
|
* @param ctx the parse tree
|
|
796
|
-
* @grammarRule KEY
|
|
797
|
-
* @return the
|
|
819
|
+
* @grammarRule KEY object_member_separator NL* value
|
|
820
|
+
* @return the object member key and value
|
|
798
821
|
*/
|
|
799
|
-
// visitObject_member?: (ctx: Object_memberContext) => Result
|
|
800
822
|
this.visitObject_member = (ctx) => {
|
|
801
823
|
(0, print_1.debugPrint)('-> Entered visitObject_member(..)');
|
|
802
824
|
const rawKey = ctx.KEY().getText();
|
|
803
825
|
const key = (0, string_1.trimBackticks)(rawKey);
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
826
|
+
const separator = this.visitObject_member_separator(ctx.object_member_separator());
|
|
827
|
+
if (separator === '=' && this.isStrict) {
|
|
828
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx.object_member_separator()), 'Syntax-Error', "Invalid inline object member separator '=' in strict mode", "Inside inline objects, members must use ':' in strict mode.", `Use '${key}: <value>' instead of '${key} = <value>'.`);
|
|
829
|
+
return {
|
|
830
|
+
key,
|
|
831
|
+
value: makeScalarValue('Undefined', undefined, 'Invalid object member separator already reported'),
|
|
832
|
+
separator,
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
const valueCtx = ctx.value();
|
|
836
|
+
const rawValue = valueCtx?.getText() ?? '';
|
|
837
|
+
const valueNode = valueCtx
|
|
838
|
+
? this.visitValue(valueCtx)
|
|
839
|
+
: makeScalarValue('Undefined', undefined, 'Missing object member value');
|
|
808
840
|
(0, print_1.debugPrint)(' rawKey = ' + rawKey);
|
|
809
841
|
(0, print_1.debugPrint)(' key = ' + key);
|
|
842
|
+
(0, print_1.debugPrint)('separator = ' + separator);
|
|
810
843
|
(0, print_1.debugPrint)('rawValue = ' + rawValue);
|
|
811
844
|
if (!valueNode) {
|
|
812
|
-
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
813
845
|
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Invalid object entry', `Invalid object entry for key '${key}'.`, `Got '${rawValue}', but expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`);
|
|
814
846
|
}
|
|
815
847
|
(0, print_1.debugPrint)('<- About to exit visitObject_member(..)');
|
|
816
848
|
if ((0, env_1.isDebug)()) {
|
|
817
849
|
console.log('Returning:');
|
|
818
|
-
(0, print_1.printObject)({ key, value: valueNode });
|
|
850
|
+
(0, print_1.printObject)({ key, value: valueNode, separator });
|
|
819
851
|
}
|
|
820
|
-
return { key, value: valueNode };
|
|
852
|
+
return { key, value: valueNode, separator };
|
|
821
853
|
};
|
|
822
854
|
/**
|
|
823
|
-
*
|
|
855
|
+
* Visit a parse tree produced by `YiniParser.object_member_separator`.
|
|
856
|
+
* @param ctx the parse tree
|
|
857
|
+
* @grammarRule COLON | EQ
|
|
858
|
+
* @return ':' or '='
|
|
824
859
|
*/
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
// this.putMember(
|
|
834
|
-
// this.errorHandler!,
|
|
835
|
-
// ctx,
|
|
836
|
-
// current,
|
|
837
|
-
// key,
|
|
838
|
-
// value,
|
|
839
|
-
// // this.ast,
|
|
840
|
-
// this.onDuplicateKey,
|
|
841
|
-
// )
|
|
842
|
-
// debugPrint('<- About to exit visitColon_list_decl(..)...')
|
|
843
|
-
// if (isDebug()) {
|
|
844
|
-
// console.log('List literal: (from a Colon-list)')
|
|
845
|
-
// printObject(value)
|
|
846
|
-
// }
|
|
847
|
-
// return value
|
|
848
|
-
// }
|
|
860
|
+
this.visitObject_member_separator = (ctx) => {
|
|
861
|
+
const sep = ctx.getText();
|
|
862
|
+
if (sep === ':' || sep === '=') {
|
|
863
|
+
return sep;
|
|
864
|
+
}
|
|
865
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Invalid inline object member separator', `Got '${sep}', but expected ':' or '='.`, "Use ':' for canonical inline object members. In lenient mode only, '=' may also be accepted.");
|
|
866
|
+
return ':';
|
|
867
|
+
};
|
|
849
868
|
/**
|
|
850
|
-
*
|
|
851
|
-
* @
|
|
852
|
-
* @
|
|
869
|
+
*
|
|
870
|
+
* @note
|
|
871
|
+
* @yini strict + active lenient => error
|
|
872
|
+
* @yini lenient + active strict => warning
|
|
873
|
+
* @yini strict + active strict => ok
|
|
874
|
+
* @yini lenient + active lenient => ok
|
|
875
|
+
* @yini + any mode => ok
|
|
853
876
|
*/
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
const
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
let txt = ''
|
|
861
|
-
try {
|
|
862
|
-
txt = parseStringLiteral(parsedInput)
|
|
863
|
-
} catch (err) {
|
|
864
|
-
const msg = '' + (<any>err)?.message
|
|
865
|
-
this.errorHandler!.pushOrBail(
|
|
866
|
-
toErrorLocation(ctx),
|
|
867
|
-
'Syntax-Error',
|
|
868
|
-
'Parse error in string',
|
|
869
|
-
`${msg}`,
|
|
870
|
-
)
|
|
877
|
+
this.visitYini_directive = (ctx) => {
|
|
878
|
+
(0, print_1.debugPrint)('-> Entered visitYini_directive(..)');
|
|
879
|
+
const modeText = this.visitYini_mode_declaration(ctx.yini_mode_declaration?.());
|
|
880
|
+
const displayText = modeText ? `@yini ${modeText}` : '@yini';
|
|
881
|
+
if (this.ast.yiniMarkerSeen) {
|
|
882
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), this.isStrict ? 'Syntax-Error' : 'Syntax-Warning', 'Duplicate YINI marker in document', `'${displayText}' appears after an earlier @yini marker. Only one YINI marker should be used.`);
|
|
871
883
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
const rawText = ctx.STRING().getText();
|
|
877
|
-
const parsedInput = this.extractStringKindAndValue(rawText);
|
|
878
|
-
try {
|
|
879
|
-
return (0, parseString_1.default)(parsedInput);
|
|
884
|
+
this.ast.yiniMarkerSeen = true;
|
|
885
|
+
this.meta_hasYiniMarker = true;
|
|
886
|
+
if (!modeText) {
|
|
887
|
+
return null;
|
|
880
888
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
889
|
+
const activeMode = this.isStrict ? 'strict' : 'lenient';
|
|
890
|
+
if (modeText !== 'strict' && modeText !== 'lenient') {
|
|
891
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx.yini_mode_declaration?.() ?? ctx), 'Syntax-Error', 'Invalid @yini mode declaration', `Got '${modeText}', but expected 'strict' or 'lenient'.`, "Use '@yini strict', '@yini lenient', or just '@yini'.");
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
894
|
+
if (modeText === 'strict' && activeMode === 'lenient') {
|
|
895
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'YINI mode declaration does not match active parser mode', 'Document declares strict mode but parser is running in lenient mode.', "Parse this document in strict mode, or change/remove the '@yini strict' declaration.");
|
|
896
|
+
return null;
|
|
886
897
|
}
|
|
898
|
+
if (modeText === 'lenient' && activeMode === 'strict') {
|
|
899
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Warning', 'YINI mode declaration does not match active parser mode', 'Document declares lenient mode but parser is running in strict mode.', 'The document remains valid if it satisfies strict-mode rules, but the declared parsing intent is lenient.');
|
|
900
|
+
}
|
|
901
|
+
return null;
|
|
902
|
+
};
|
|
903
|
+
this.visitYini_mode_declaration = (ctx) => {
|
|
904
|
+
if (!ctx)
|
|
905
|
+
return undefined;
|
|
906
|
+
return ctx.getText().trim().toLowerCase();
|
|
887
907
|
};
|
|
888
908
|
/**
|
|
889
909
|
* Visit a parse tree produced by `YiniParser.bad_member`.
|
|
@@ -916,14 +936,10 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
916
936
|
this.options = options;
|
|
917
937
|
this.errorHandler = errorHandler;
|
|
918
938
|
this.isStrict = options?.rules?.initialMode === 'strict';
|
|
919
|
-
this.onDuplicateKey =
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
// } else {
|
|
923
|
-
// this.onDuplicateKey = 'warn'
|
|
924
|
-
// }
|
|
939
|
+
this.onDuplicateKey =
|
|
940
|
+
options?.rules?.onDuplicateKey ??
|
|
941
|
+
(this.isStrict ? 'error' : 'warn-and-keep-first');
|
|
925
942
|
const root = makeSection('(root)', 0);
|
|
926
|
-
// this.mapSectionNamePaths.set('(root)', 0)
|
|
927
943
|
this.ast = {
|
|
928
944
|
root,
|
|
929
945
|
isStrict: this.isStrict,
|
|
@@ -954,69 +970,21 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
954
970
|
this.errorHandler.pushOrBail(undefined, 'Syntax-Error', 'Strict mode requires exactly one explicit top-level section.', `Found ${numTopLevelSections} explicit top-level section${numTopLevelSections === 1 ? '' : 's'}.`, 'Wrap the document in exactly one explicit top-level section and nest any additional sections beneath it.');
|
|
955
971
|
}
|
|
956
972
|
}
|
|
957
|
-
extractStringParts(tokenText) {
|
|
958
|
-
// Detect prefix
|
|
959
|
-
let prefix = '';
|
|
960
|
-
let rest = tokenText;
|
|
961
|
-
const prefixMatch = tokenText.match(/^(C|c|H|h|R|r)/);
|
|
962
|
-
if (prefixMatch) {
|
|
963
|
-
prefix = prefixMatch[1].toUpperCase();
|
|
964
|
-
rest = tokenText.slice(1);
|
|
965
|
-
}
|
|
966
|
-
// Triple quoted
|
|
967
|
-
if (rest.startsWith('"""')) {
|
|
968
|
-
const inner = rest.slice(3, -3);
|
|
969
|
-
if (prefix === 'C') {
|
|
970
|
-
return { strKind: 'triple-classic', value: inner };
|
|
971
|
-
}
|
|
972
|
-
return { strKind: 'triple-raw', value: inner };
|
|
973
|
-
}
|
|
974
|
-
// Single quoted or double quoted
|
|
975
|
-
const quote = rest[0];
|
|
976
|
-
const inner = rest.slice(1, -1);
|
|
977
|
-
switch (prefix) {
|
|
978
|
-
case 'C':
|
|
979
|
-
return { strKind: 'classic', value: inner };
|
|
980
|
-
case 'H':
|
|
981
|
-
return { strKind: 'hyper', value: inner };
|
|
982
|
-
case 'R':
|
|
983
|
-
case '':
|
|
984
|
-
return { strKind: 'raw', value: inner };
|
|
985
|
-
default:
|
|
986
|
-
return { strKind: 'raw', value: inner };
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
973
|
extractStringKindAndValue(raw) {
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
prefix = 'C';
|
|
1000
|
-
else if (/^[Hh]/.test(raw))
|
|
1001
|
-
prefix = 'H';
|
|
1002
|
-
else if (/^[Rr]/.test(raw))
|
|
1003
|
-
prefix = 'R';
|
|
1004
|
-
if (raw.startsWith('"""') ||
|
|
1005
|
-
raw.startsWith('C"""') ||
|
|
1006
|
-
raw.startsWith('c"""')) {
|
|
1007
|
-
value = raw.replace(/^[CRcr]?"""/, '').replace(/"""$/, '');
|
|
1008
|
-
strKind = prefix === 'C' ? 'triple-classic' : 'triple-raw';
|
|
974
|
+
const hasClassicPrefix = /^[Cc]/.test(raw);
|
|
975
|
+
const hasRawPrefix = /^[Rr]/.test(raw);
|
|
976
|
+
const hasPrefix = hasClassicPrefix || hasRawPrefix;
|
|
977
|
+
const body = hasPrefix ? raw.slice(1) : raw;
|
|
978
|
+
if (body.startsWith('"""')) {
|
|
979
|
+
return {
|
|
980
|
+
strKind: hasClassicPrefix ? 'triple-classic' : 'triple-raw',
|
|
981
|
+
value: body.slice(3, -3),
|
|
982
|
+
};
|
|
1009
983
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
else if (prefix === 'H')
|
|
1015
|
-
strKind = 'hyper';
|
|
1016
|
-
else
|
|
1017
|
-
strKind = 'raw';
|
|
1018
|
-
}
|
|
1019
|
-
return { strKind, value };
|
|
984
|
+
return {
|
|
985
|
+
strKind: hasClassicPrefix ? 'classic' : 'raw',
|
|
986
|
+
value: body.slice(1, -1),
|
|
987
|
+
};
|
|
1020
988
|
}
|
|
1021
989
|
/** Attach a section to the stack respecting up/down moves (Spec 5.3). :contentReference[oaicite:7]{index=7} */
|
|
1022
990
|
attachSection(ctx, stack, section, ast) {
|
|
@@ -1025,7 +993,7 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
1025
993
|
if (targetLevel <= 0) {
|
|
1026
994
|
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
1027
995
|
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Warning', `Invalid section level: ${targetLevel}`);
|
|
1028
|
-
return;
|
|
996
|
+
return false;
|
|
1029
997
|
}
|
|
1030
998
|
// ------------------------------
|
|
1031
999
|
// Construct section name path.
|
|
@@ -1041,12 +1009,15 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
1041
1009
|
(0, print_1.debugPrint)('section full path: keyPath = ' + keyPath);
|
|
1042
1010
|
// ------------------------------
|
|
1043
1011
|
if (this.hasDefinedSectionTitle(keyPath)) {
|
|
1044
|
-
|
|
1045
|
-
|
|
1012
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), this.isStrict ? 'Syntax-Error' : 'Syntax-Warning', 'Duplicate section name', `Section name: '${sectionName}' at level ${targetLevel} is already defined and cannot be redefined.`, this.isStrict
|
|
1013
|
+
? 'Duplicate sections are not allowed in strict mode.'
|
|
1014
|
+
: 'The first section definition is kept; later duplicate sections are ignored.');
|
|
1015
|
+
this.ignoredSectionLevel = targetLevel;
|
|
1016
|
+
return false;
|
|
1046
1017
|
}
|
|
1047
1018
|
else {
|
|
1048
1019
|
if (section.members === undefined) {
|
|
1049
|
-
(0, print_1.debugPrint)('This
|
|
1020
|
+
(0, print_1.debugPrint)('This section result does not hold any valid members (=undefined)');
|
|
1050
1021
|
}
|
|
1051
1022
|
else {
|
|
1052
1023
|
// this.existingSectionTitles.set(key, true)
|
|
@@ -1066,6 +1037,20 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
1066
1037
|
if (targetLevel > this.meta_maxLevel) {
|
|
1067
1038
|
this.meta_maxLevel = targetLevel;
|
|
1068
1039
|
}
|
|
1040
|
+
return true;
|
|
1041
|
+
}
|
|
1042
|
+
isIgnoringDuplicateSection() {
|
|
1043
|
+
return this.ignoredSectionLevel !== null;
|
|
1044
|
+
}
|
|
1045
|
+
shouldSkipSectionAtLevel(sectionLevel) {
|
|
1046
|
+
if (this.ignoredSectionLevel === null) {
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
if (sectionLevel > this.ignoredSectionLevel) {
|
|
1050
|
+
return true;
|
|
1051
|
+
}
|
|
1052
|
+
this.ignoredSectionLevel = null;
|
|
1053
|
+
return false;
|
|
1069
1054
|
}
|
|
1070
1055
|
/** Insert a key/value into current section (duplicate handling per options). */
|
|
1071
1056
|
putMember(errorHandler, ctx, sec, key, value, mode = 'warn-and-keep-first') {
|
|
@@ -1097,6 +1082,53 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
1097
1082
|
}
|
|
1098
1083
|
sec.members.set(key, value);
|
|
1099
1084
|
}
|
|
1085
|
+
stringifyConcatOperand(value) {
|
|
1086
|
+
switch (value.type) {
|
|
1087
|
+
case 'String':
|
|
1088
|
+
return value.value;
|
|
1089
|
+
case 'Number':
|
|
1090
|
+
return String(value.value);
|
|
1091
|
+
case 'Boolean':
|
|
1092
|
+
return value.value ? 'true' : 'false';
|
|
1093
|
+
case 'Null':
|
|
1094
|
+
return 'null';
|
|
1095
|
+
default:
|
|
1096
|
+
return '';
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
parseStringToken(tokenText, ctx) {
|
|
1100
|
+
const parsed = this.extractStringKindAndValue(tokenText);
|
|
1101
|
+
try {
|
|
1102
|
+
const value = (0, parseString_1.default)(parsed);
|
|
1103
|
+
return makeScalarValue('String', value);
|
|
1104
|
+
}
|
|
1105
|
+
catch (err) {
|
|
1106
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1107
|
+
let msgWhat = 'Parse error in string';
|
|
1108
|
+
let msgWhy = msg;
|
|
1109
|
+
let msgHint = '';
|
|
1110
|
+
if (err instanceof parseString_1.CYiniStringParseError) {
|
|
1111
|
+
if (/Invalid escape sequence/i.test(msg)) {
|
|
1112
|
+
msgWhat = 'Invalid escape sequence in string';
|
|
1113
|
+
msgHint =
|
|
1114
|
+
'Use double backslashes (\\\\) in C-strings, or use a raw string if escapes are not needed.';
|
|
1115
|
+
}
|
|
1116
|
+
else if (/end of string/i.test(msg)) {
|
|
1117
|
+
msgWhat = 'Incomplete escape sequence in string';
|
|
1118
|
+
msgHint =
|
|
1119
|
+
'Check that all escape sequences in the C-string are complete and valid.';
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', msgWhat, msgWhy, msgHint);
|
|
1123
|
+
return makeScalarValue('Undefined', undefined, 'Invalid string literal already reported');
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
hasTrailingComma(ctx) {
|
|
1127
|
+
const childCount = ctx.getChildCount?.() ?? 0;
|
|
1128
|
+
if (childCount <= 0)
|
|
1129
|
+
return false;
|
|
1130
|
+
return ctx.getChild(childCount - 1)?.getText?.() === ',';
|
|
1131
|
+
}
|
|
1100
1132
|
// --------------------------------
|
|
1101
1133
|
// Public entry
|
|
1102
1134
|
buildAST(ctx) {
|