yini-parser 1.4.3 → 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 +52 -0
- package/README.md +187 -35
- package/dist/YINI.d.ts +29 -18
- package/dist/YINI.js +104 -3
- package/dist/core/astBuilder.d.ts +94 -18
- package/dist/core/astBuilder.js +439 -376
- 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.d.ts +8 -4
- package/dist/core/objectBuilder.js +47 -62
- package/dist/core/options/defaultParserOptions.d.ts +3 -2
- package/dist/core/options/defaultParserOptions.js +11 -2
- package/dist/core/options/optionsFunctions.js +6 -4
- package/dist/core/parsingRules/modeFromRulesMatcher.d.ts +1 -1
- package/dist/core/parsingRules/modeFromRulesMatcher.js +22 -13
- package/dist/core/pipeline/pipeline.js +35 -10
- package/dist/core/runtime.js +28 -19
- package/dist/grammar/generated/YiniLexer.d.ts +40 -53
- package/dist/grammar/generated/YiniLexer.js +357 -356
- package/dist/grammar/generated/YiniParser.d.ts +174 -118
- package/dist/grammar/generated/YiniParser.js +1185 -929
- package/dist/grammar/generated/YiniParserVisitor.d.ts +82 -19
- 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 +20 -3
- 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 -168
- 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,12 +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"));
|
|
47
|
+
const parseNull_1 = __importDefault(require("../parsers/parseNull"));
|
|
43
48
|
const parseNumber_1 = __importDefault(require("../parsers/parseNumber"));
|
|
44
|
-
// import parseNumber from '../parsers/parseNumber'
|
|
45
49
|
const parseSectionHeader_1 = __importDefault(require("../parsers/parseSectionHeader"));
|
|
46
50
|
const parseString_1 = __importStar(require("../parsers/parseString"));
|
|
47
51
|
const number_1 = require("../utils/number");
|
|
@@ -71,6 +75,7 @@ const makeScalarValue = (type, value = null, tag = undefined) => {
|
|
|
71
75
|
case 'Undefined':
|
|
72
76
|
return { type: 'Undefined', value: undefined, tag };
|
|
73
77
|
default:
|
|
78
|
+
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
74
79
|
new errorDataHandler_1.ErrorDataHandler(_sourceType).pushOrBail(undefined, 'Fatal-Error', `No such type in makeValue(..), type: ${type}, value: ${value}`, 'Something in the code is done incorrectly in order for this to happen... :S');
|
|
75
80
|
}
|
|
76
81
|
return { type: 'Null', value: null, tag };
|
|
@@ -91,54 +96,9 @@ const makeListValue = (elems = [], tag = undefined) => {
|
|
|
91
96
|
const makeObjectValue = (entries = {}, tag = undefined) => {
|
|
92
97
|
return { type: 'Object', entries, tag };
|
|
93
98
|
};
|
|
94
|
-
function trimQuotes(text) {
|
|
95
|
-
// STRING token already excludes quotes; the rule returns the literal with quotes present.
|
|
96
|
-
// We’ll reliably strip the outer quote(s) and leave contents as-is (concat pieces handled below).
|
|
97
|
-
const q = text[0];
|
|
98
|
-
if ((q === '"' || q === "'") &&
|
|
99
|
-
text.length >= 2 &&
|
|
100
|
-
text[text.length - 1] === q) {
|
|
101
|
-
return text.slice(1, -1);
|
|
102
|
-
}
|
|
103
|
-
// Triple-quoted cases are handled by the lexer too; same stripping works since token text begins with quotes.
|
|
104
|
-
if (text.startsWith('"""') && text.endsWith('"""') && text.length >= 6) {
|
|
105
|
-
return text.slice(3, -3);
|
|
106
|
-
}
|
|
107
|
-
return text;
|
|
108
|
-
}
|
|
109
99
|
function makeSection(name, level) {
|
|
110
100
|
return { sectionName: name, level, members: new Map(), children: [] };
|
|
111
101
|
}
|
|
112
|
-
/** Parse SECTION_HEAD token text → {level, name}.
|
|
113
|
-
* Supports repeated markers (^^^^) and shorthand (^7) (Spec 5.2–5.3.1). :contentReference[oaicite:5]{index=5}:contentReference[oaicite:6]{index=6}
|
|
114
|
-
*/
|
|
115
|
-
// function parseSectionHeadToken(raw: string): { level: number; name: string } {
|
|
116
|
-
// // SECTION_HEAD token text includes: optional WS, marker(s) or shorthand, WS, IDENT (possibly backticked), NL+
|
|
117
|
-
// // We only need the visible line content up to NL.
|
|
118
|
-
// const line = raw.split(/\r?\n/)[0]
|
|
119
|
-
// // Extract marker block and name
|
|
120
|
-
// // Examples: "^^ Section", "^7 `Section name`", "< MySection"
|
|
121
|
-
// const m = line.match(/^\s*([\^<§€]+|\^|\<|§|€)(\d+)?[ \t]+(.+?)\s*$/)
|
|
122
|
-
// if (m) {
|
|
123
|
-
// const markerRun = m[1]
|
|
124
|
-
// const numeric = m[2]
|
|
125
|
-
// let level: number
|
|
126
|
-
// if (numeric) {
|
|
127
|
-
// level = parseInt(numeric, 10)
|
|
128
|
-
// } else {
|
|
129
|
-
// // count repeated marker chars (^^^^)
|
|
130
|
-
// level = markerRun.length
|
|
131
|
-
// }
|
|
132
|
-
// // Section name may be backticked: `Name with spaces`
|
|
133
|
-
// let name = m[3]
|
|
134
|
-
// if (name.startsWith('`') && name.endsWith('`')) {
|
|
135
|
-
// name = name.slice(1, -1)
|
|
136
|
-
// }
|
|
137
|
-
// return { level, name }
|
|
138
|
-
// }
|
|
139
|
-
// // Fallback: be defensive
|
|
140
|
-
// return { level: 1, name: line.trim() }
|
|
141
|
-
// }
|
|
142
102
|
// --- Builder Visitor -----------------------------------------------------
|
|
143
103
|
/**
|
|
144
104
|
* This interface defines a complete generic visitor for a parse tree produced
|
|
@@ -148,6 +108,7 @@ function makeSection(name, level) {
|
|
|
148
108
|
* operations with no return type.
|
|
149
109
|
*/
|
|
150
110
|
// export default class YINIVisitor<IResult> extends YiniParserVisitor<IResult> {
|
|
111
|
+
// export default class ASTBuilder<Result> extends YiniParserVisitor<Result> {
|
|
151
112
|
class ASTBuilder extends YiniParserVisitor_1.default {
|
|
152
113
|
/**
|
|
153
114
|
* @param metaFileName If parsing from a file, provide the file name here so the meta information can be updated accordingly.
|
|
@@ -156,13 +117,13 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
156
117
|
constructor(errorHandler, options, sourceType, metaFileName) {
|
|
157
118
|
super();
|
|
158
119
|
this.errorHandler = null;
|
|
120
|
+
this.ignoredSectionLevel = null;
|
|
159
121
|
this.meta_hasYiniMarker = false; // For stats.
|
|
160
122
|
// private meta_numOfSections = 0 // For stats.
|
|
161
123
|
this._numOfMembers = 0; // For error checking and stats.
|
|
162
124
|
// private meta_numOfChains = 0 // For stats.
|
|
163
125
|
this.meta_maxLevel = 0; // For stats.
|
|
164
126
|
this.mapSectionNamePaths = new Map();
|
|
165
|
-
// --- Private helper methods --------------------------------
|
|
166
127
|
this.hasDefinedSectionTitle = (keyPath) => {
|
|
167
128
|
return this.mapSectionNamePaths?.has(keyPath);
|
|
168
129
|
};
|
|
@@ -174,8 +135,6 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
174
135
|
* @param ctx the parse tree
|
|
175
136
|
* @return the visitor result
|
|
176
137
|
*/
|
|
177
|
-
// visitYini?: (ctx: YiniContext) => Result
|
|
178
|
-
// visitYini?: (ctx: YiniContext) => any
|
|
179
138
|
this.visitYini = (ctx) => {
|
|
180
139
|
// children: prolog?, stmt*, terminal?, EOF
|
|
181
140
|
ctx.children?.forEach((c) => this.visit?.(c));
|
|
@@ -196,50 +155,87 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
196
155
|
* @param ctx the parse tree
|
|
197
156
|
* @return the visitor result
|
|
198
157
|
*/
|
|
199
|
-
// visitTerminal_stmt?: (ctx: Terminal_stmtContext) => Result;
|
|
200
158
|
this.visitTerminal_stmt = (ctx) => {
|
|
201
159
|
(0, print_1.debugPrint)('-> Entered visitTerminal_stmt(..)');
|
|
202
160
|
let rawText = ctx.getText().trim();
|
|
203
161
|
(0, print_1.debugPrint)('rawText = "' + rawText + '"');
|
|
204
|
-
// rawText = extractYiniLine(rawText) // Remove possible comments.
|
|
205
|
-
// rawText = stripCommentsAndAfter(rawText.split('\n', 1)[0]).trim() // Remove possible comments.
|
|
206
162
|
rawText = (0, yiniHelpers_1.stripCommentsAndAfter)(rawText); // Remove possible comments.
|
|
207
163
|
(0, print_1.debugPrint)('rawText2 = "' + rawText + '"');
|
|
208
164
|
if (rawText.toLowerCase() === '/end') {
|
|
209
165
|
// NOTE: Below, maybe not reached at all.
|
|
210
166
|
if (this.ast.terminatorSeen) {
|
|
211
167
|
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
212
|
-
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.`);
|
|
213
169
|
}
|
|
214
170
|
}
|
|
215
171
|
else {
|
|
216
172
|
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
217
|
-
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).`);
|
|
218
174
|
}
|
|
219
175
|
this.ast.terminatorSeen = true;
|
|
220
176
|
return null;
|
|
221
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
|
+
};
|
|
222
187
|
/**
|
|
223
188
|
* Visit a parse tree produced by `YiniParser.stmt`.
|
|
224
189
|
* @param ctx the parse tree
|
|
225
|
-
* @grammarRule eol | SECTION_HEAD | assignment | colon_list_decl | marker_stmt | bad_member
|
|
226
190
|
* @return the visitor result
|
|
227
191
|
*/
|
|
228
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
|
+
*/
|
|
229
215
|
this.visitStmt = (ctx) => {
|
|
230
216
|
const child = ctx.getChild(0);
|
|
231
217
|
const ruleName = child?.constructor?.name ?? '';
|
|
218
|
+
// debugPrint('S0')
|
|
219
|
+
// const badHeaderWDotName = ctx.BAD_SECTION_HEAD_W_DOT_NAME()?.getText()
|
|
220
|
+
// if (badHeaderWDotName) {
|
|
221
|
+
// console.log('QQQQQQQQ = ' + badHeaderWDotName)
|
|
222
|
+
// }
|
|
232
223
|
if (ruleName.includes('EolContext'))
|
|
233
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);
|
|
234
234
|
if (ruleName.includes('AssignmentContext'))
|
|
235
235
|
return this.visitAssignment?.(child);
|
|
236
|
-
if (ruleName.includes('Colon_list_declContext'))
|
|
237
|
-
return this.visitColon_list_decl?.(child);
|
|
238
236
|
if (ruleName.includes('Meta_stmtContext'))
|
|
239
237
|
return this.visitMeta_stmt?.(child);
|
|
240
238
|
(0, print_1.debugPrint)('S1');
|
|
241
|
-
// let headerAlt = child.getText?.() ?? ''
|
|
242
|
-
// let header = ctx.SECTION_HEAD()?.getText().trim() || ''
|
|
243
239
|
let header = ctx.SECTION_HEAD()?.getText().trim() || '';
|
|
244
240
|
// debugPrint('S2, lineAlt: >>>' + lineAlt + '<<<')
|
|
245
241
|
(0, print_1.debugPrint)('S2, header: >>>' + header + '<<<');
|
|
@@ -247,6 +243,9 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
247
243
|
(0, print_1.debugPrint)('S3, header: >>>' + header + '<<<');
|
|
248
244
|
if (!!header) {
|
|
249
245
|
const { sectionName, sectionLevel } = (0, parseSectionHeader_1.default)(header, this.errorHandler, ctx);
|
|
246
|
+
if (this.shouldSkipSectionAtLevel(sectionLevel)) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
250
249
|
// Validate level sequencing per spec 5.3 (no skipping upward)
|
|
251
250
|
const currentLevel = this.sectionStack[this.sectionStack.length - 1].level;
|
|
252
251
|
if (sectionLevel > currentLevel + 1) {
|
|
@@ -260,6 +259,10 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
260
259
|
// bad_member fallback
|
|
261
260
|
return this.visitBad_member?.(ctx.getChild(0));
|
|
262
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
|
+
};
|
|
263
266
|
/**
|
|
264
267
|
* Visit a parse tree produced by `YiniParser.meta_stmt`.
|
|
265
268
|
* @param ctx the parse tree
|
|
@@ -284,31 +287,19 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
284
287
|
*/
|
|
285
288
|
this.visitDirective = (ctx) => {
|
|
286
289
|
(0, print_1.debugPrint)('-> Entered visitDirective(..)');
|
|
287
|
-
|
|
288
|
-
(0, print_1.debugPrint)('rawText = "' + rawText + '"');
|
|
289
|
-
// NOTE: Important to strip any possible comments on the same line.
|
|
290
|
-
rawText = (0, yiniHelpers_1.stripCommentsAndAfter)(rawText); // Remove possible comments.
|
|
291
|
-
(0, print_1.debugPrint)('rawText2 = "' + rawText + '"');
|
|
290
|
+
const rawText = (0, yiniHelpers_1.stripCommentsAndAfter)(ctx.getText().trim());
|
|
292
291
|
if (this.mapSectionNamePaths.size || this._numOfMembers) {
|
|
293
|
-
|
|
294
|
-
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.');
|
|
295
293
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
this.
|
|
299
|
-
}
|
|
300
|
-
else if (rawText.toLowerCase() === '@yini') {
|
|
301
|
-
if (this.ast.yiniMarkerSeen) {
|
|
302
|
-
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
303
|
-
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').`);
|
|
304
|
-
}
|
|
294
|
+
const yiniDirective = ctx.yini_directive?.();
|
|
295
|
+
if (yiniDirective) {
|
|
296
|
+
return this.visitYini_directive(yiniDirective);
|
|
305
297
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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;
|
|
309
301
|
}
|
|
310
|
-
|
|
311
|
-
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.");
|
|
312
303
|
return null;
|
|
313
304
|
};
|
|
314
305
|
/**
|
|
@@ -404,10 +395,6 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
404
395
|
*/
|
|
405
396
|
let valueContext = ctx.value?.();
|
|
406
397
|
let valueNode;
|
|
407
|
-
const hasEquals = rawMemberText.includes('=');
|
|
408
|
-
// const hasTextAfterEquals = hasEquals
|
|
409
|
-
// ? rawMemberText.split('=').slice(1).join('=').trim().length > 0
|
|
410
|
-
// : false
|
|
411
398
|
const eqIndex = rawMemberText.indexOf('=');
|
|
412
399
|
const hasTextAfterEquals = eqIndex >= 0 && rawMemberText.slice(eqIndex + 1).trim().length > 0;
|
|
413
400
|
if (!valueContext || !rawValue) {
|
|
@@ -423,9 +410,11 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
423
410
|
break;
|
|
424
411
|
case 'allow-with-warning':
|
|
425
412
|
valueNode = makeScalarValue('Null', null, 'Implicit null (empty value)');
|
|
413
|
+
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
426
414
|
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Warning', `Empty value treated as null for key '${resultKey}'.`, `An empty value after '=' was encountered. Per 'treatEmptyValueAsNull = allow-with-warning', interpreted as null.`, `If you intended null, write it explicitly: ${resultKey} = null. Otherwise provide a non-empty value or set 'treatEmptyValueAsNull' to 'disallow'.`);
|
|
427
415
|
break;
|
|
428
416
|
case 'disallow':
|
|
417
|
+
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
429
418
|
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', `Missing value for key '${resultKey}'`, `Expected a value after '=' but found none. Implicit nulls are disallowed by 'treatEmptyValueAsNull = disallow'.`, `Write 'null' explicitly (${resultKey} = null) if that is intended, or provide a concrete value.`);
|
|
430
419
|
return makeScalarValue('Undefined', undefined, 'Missing value already reported');
|
|
431
420
|
}
|
|
@@ -437,34 +426,15 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
437
426
|
if ((0, env_1.isDebug)()) {
|
|
438
427
|
(0, print_1.printObject)(valueNode);
|
|
439
428
|
}
|
|
440
|
-
/*
|
|
441
|
-
if (!valueNode) {
|
|
442
|
-
this.errorHandler!.pushOrBail(
|
|
443
|
-
toErrorLocation(ctx),
|
|
444
|
-
'Syntax-Error',
|
|
445
|
-
'Invalid value',
|
|
446
|
-
`Invalid value for key '${resultKey}' in member (<key> = <value> pair).`,
|
|
447
|
-
`Got '${rawValue}', but expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`,
|
|
448
|
-
)
|
|
449
|
-
} else if (
|
|
450
|
-
valueNode.type === 'Undefined' &&
|
|
451
|
-
valueNode.tag !== 'Invalid string literal already reported'
|
|
452
|
-
) {
|
|
453
|
-
this.errorHandler!.pushOrBail(
|
|
454
|
-
toErrorLocation(ctx),
|
|
455
|
-
'Syntax-Error',
|
|
456
|
-
'Invalid value',
|
|
457
|
-
`Invalid value for key '${resultKey}' in member (<key> = <value> pair).`,
|
|
458
|
-
`Got '${rawValue}', but expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`,
|
|
459
|
-
)
|
|
460
|
-
}*/
|
|
461
429
|
if (!valueNode) {
|
|
430
|
+
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
462
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 '-'.`);
|
|
463
432
|
}
|
|
464
433
|
else if (valueNode.type === 'Undefined' &&
|
|
465
434
|
valueNode.tag !== 'Invalid string literal already reported' &&
|
|
466
435
|
valueNode.tag !== 'Missing value already reported' &&
|
|
467
436
|
valueNode.tag !== 'Parser syntax error already reported') {
|
|
437
|
+
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
468
438
|
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 '-'.`);
|
|
469
439
|
}
|
|
470
440
|
// It keeps the sentinel tags useful for control flow inside visitMember(..),
|
|
@@ -484,26 +454,17 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
484
454
|
* @param ctx the parse tree
|
|
485
455
|
* @return the visitor result
|
|
486
456
|
*/
|
|
487
|
-
// visitValue?: (ctx: ValueContext) => Result
|
|
488
457
|
this.visitValue = (ctx) => {
|
|
489
458
|
(0, print_1.debugPrint)('----------------------------');
|
|
490
459
|
(0, print_1.debugPrint)('-> Entered visitValue(..)');
|
|
491
460
|
let valueNode = undefined;
|
|
492
|
-
if (ctx.
|
|
493
|
-
(0, print_1.debugPrint)(' visiting
|
|
494
|
-
valueNode = this.
|
|
495
|
-
}
|
|
496
|
-
else if (ctx.string_literal()) {
|
|
497
|
-
(0, print_1.debugPrint)(' visiting visitString_literal(..)');
|
|
498
|
-
valueNode = this.visitString_literal(ctx.string_literal());
|
|
461
|
+
if (ctx.concat_expression()) {
|
|
462
|
+
(0, print_1.debugPrint)(' visiting visitConcat_expression(..)');
|
|
463
|
+
valueNode = this.visitConcat_expression(ctx.concat_expression());
|
|
499
464
|
}
|
|
500
|
-
else if (ctx.
|
|
501
|
-
(0, print_1.debugPrint)(' visiting
|
|
502
|
-
valueNode = this.
|
|
503
|
-
}
|
|
504
|
-
else if (ctx.boolean_literal()) {
|
|
505
|
-
(0, print_1.debugPrint)(' visiting visitBoolean_literal(..)');
|
|
506
|
-
valueNode = this.visitBoolean_literal(ctx.boolean_literal());
|
|
465
|
+
else if (ctx.scalar_value()) {
|
|
466
|
+
(0, print_1.debugPrint)(' visiting visitScalar_value(..)');
|
|
467
|
+
valueNode = this.visitScalar_value(ctx.scalar_value());
|
|
507
468
|
}
|
|
508
469
|
else if (ctx.list_literal()) {
|
|
509
470
|
(0, print_1.debugPrint)(' visiting visitList_literal(..)');
|
|
@@ -525,108 +486,164 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
525
486
|
return valueNode;
|
|
526
487
|
};
|
|
527
488
|
/**
|
|
528
|
-
* Visit a parse tree produced by `YiniParser.
|
|
489
|
+
* Visit a parse tree produced by `YiniParser.scalar_value`.
|
|
529
490
|
* @param ctx the parse tree
|
|
530
491
|
* @return the visitor result
|
|
531
492
|
*/
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
const pieces = [
|
|
537
|
-
ctx.STRING(),
|
|
538
|
-
...(ctx.string_concat_list()?.map((c) => c.STRING()) ?? []),
|
|
539
|
-
]
|
|
540
|
-
|
|
541
|
-
try {
|
|
542
|
-
for (const token of pieces) {
|
|
543
|
-
const tokenText = token.getText()
|
|
544
|
-
const parsed = this.extractStringKindAndValue(tokenText)
|
|
545
|
-
|
|
546
|
-
try {
|
|
547
|
-
text += parseStringLiteral(parsed)
|
|
548
|
-
} catch (err: unknown) {
|
|
549
|
-
const msg = '' + (<any>err)?.message
|
|
550
|
-
this.errorHandler!.pushOrBail(
|
|
551
|
-
toErrorLocation(ctx),
|
|
552
|
-
'Syntax-Error',
|
|
553
|
-
'Parse error in string',
|
|
554
|
-
`${msg}`,
|
|
555
|
-
)
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
return makeScalarValue('String', text)
|
|
560
|
-
} catch (err: unknown) {
|
|
561
|
-
const msg = err instanceof Error ? err.message : String(err)
|
|
562
|
-
|
|
563
|
-
let msgWhat = 'Invalid string literal'
|
|
564
|
-
let msgWhy = msg
|
|
565
|
-
let msgHint = ''
|
|
566
|
-
|
|
567
|
-
if (err instanceof CYiniStringParseError) {
|
|
568
|
-
msgWhat = 'Invalid escape sequence in string'
|
|
569
|
-
msgWhy = msg
|
|
570
|
-
|
|
571
|
-
if (/Invalid escape sequence \\\\/.test(msg)) {
|
|
572
|
-
msgHint =
|
|
573
|
-
'Use double backslashes (\\\\) in C-strings, or use a raw string for file paths.'
|
|
574
|
-
} else if (/end of string/i.test(msg)) {
|
|
575
|
-
msgHint =
|
|
576
|
-
'Check that all escape sequences in the C-string are complete and valid.'
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
this.errorHandler!.pushOrBail(
|
|
581
|
-
toErrorLocation(ctx),
|
|
582
|
-
'Syntax-Error',
|
|
583
|
-
msgWhat,
|
|
584
|
-
msgWhy,
|
|
585
|
-
msgHint,
|
|
586
|
-
)
|
|
587
|
-
|
|
588
|
-
return makeScalarValue(
|
|
589
|
-
'Undefined',
|
|
590
|
-
undefined,
|
|
591
|
-
'Invalid string literal',
|
|
592
|
-
)
|
|
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());
|
|
593
497
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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');
|
|
607
549
|
}
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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');
|
|
627
591
|
}
|
|
592
|
+
return makeScalarValue('Number', parsedNum.value, parsedNum.tag);
|
|
628
593
|
}
|
|
629
|
-
|
|
594
|
+
if (ctx.BOOLEAN_TRUE() || ctx.BOOLEAN_FALSE()) {
|
|
595
|
+
return makeScalarValue('Boolean', (0, parseBoolean_1.default)(ctx.getText()));
|
|
596
|
+
}
|
|
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);
|
|
630
647
|
};
|
|
631
648
|
/**
|
|
632
649
|
* Visit a parse tree produced by `YiniParser.number_literal`.
|
|
@@ -687,8 +704,14 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
687
704
|
// visitNull_literal?: (ctx: Null_literalContext) => Result
|
|
688
705
|
this.visitNull_literal = (ctx) => {
|
|
689
706
|
(0, print_1.debugPrint)('-> Entered visitNull_literal(..)');
|
|
690
|
-
|
|
691
|
-
return makeScalarValue('Null', null, 'Explicit Null')
|
|
707
|
+
// debugPrint('raw = ' + ctx.getText())
|
|
708
|
+
// return makeScalarValue('Null', null, 'Explicit Null')
|
|
709
|
+
const raw = ctx.getText();
|
|
710
|
+
(0, print_1.debugPrint)('raw: "' + raw + '"');
|
|
711
|
+
const parsed = (0, parseNull_1.default)(raw);
|
|
712
|
+
(0, print_1.debugPrint)('parsed: "' + parsed + '"');
|
|
713
|
+
// Case-insensitive true/false/on/off/yes/no (Spec section, 8.1).
|
|
714
|
+
return makeScalarValue('Null', parsed, 'Explicit Null');
|
|
692
715
|
};
|
|
693
716
|
/**
|
|
694
717
|
* Visit a parse tree produced by `YiniParser.list_literal`.
|
|
@@ -699,8 +722,7 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
699
722
|
// visitList_literal?: (ctx: List_literalContext) => Result
|
|
700
723
|
this.visitList_literal = (ctx) => {
|
|
701
724
|
(0, print_1.debugPrint)('-> Entered visitList_literal(..)');
|
|
702
|
-
|
|
703
|
-
const elems = this.visitElements(ctx.elements());
|
|
725
|
+
const elems = ctx.elements() ? this.visitElements(ctx.elements()) : [];
|
|
704
726
|
const value = makeListValue(elems, 'From bracketed list');
|
|
705
727
|
(0, print_1.debugPrint)('<- About to exit visitList_literal(..)...');
|
|
706
728
|
if ((0, env_1.isDebug)()) {
|
|
@@ -718,12 +740,14 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
718
740
|
this.visitElements = (ctx) => {
|
|
719
741
|
(0, print_1.debugPrint)('-> Entered visitElements(..)');
|
|
720
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
|
+
}
|
|
721
746
|
const elems = !ctx?.value_list()
|
|
722
747
|
? []
|
|
723
748
|
: ctx.value_list().map((elem) => {
|
|
724
749
|
const valueNode = this.visitValue(elem);
|
|
725
750
|
if (!valueNode) {
|
|
726
|
-
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
727
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 '-'.`);
|
|
728
752
|
}
|
|
729
753
|
return valueNode;
|
|
@@ -743,10 +767,6 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
743
767
|
*/
|
|
744
768
|
this.visitObject_literal = (ctx) => {
|
|
745
769
|
(0, print_1.debugPrint)('-> Entered visitObject_literal(..)');
|
|
746
|
-
// debugPrint('entries.EMPTY_OBJECT = ' + ctx?.EMPTY_OBJECT())
|
|
747
|
-
// debugPrint('entries.length = ' + ctx?.object_members())
|
|
748
|
-
// printObject(ctx)
|
|
749
|
-
// const entries = this.visitObject_members(ctx?.object_members())
|
|
750
770
|
const entries = ctx.object_members()
|
|
751
771
|
? this.visitObject_members(ctx.object_members())
|
|
752
772
|
: {};
|
|
@@ -763,110 +783,127 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
763
783
|
* @param ctx the parse tree
|
|
764
784
|
* @grammarRule object_member (COMMA NL* object_member)* COMMA?
|
|
765
785
|
* @return the visitor result
|
|
786
|
+
*
|
|
787
|
+
* @note
|
|
788
|
+
* SPEC says:
|
|
789
|
+
* - Lenient: first wins + warning
|
|
790
|
+
* - Strict: error
|
|
791
|
+
*
|
|
792
|
+
* NEVER silently overwrite.
|
|
766
793
|
*/
|
|
767
794
|
this.visitObject_members = (ctx) => {
|
|
768
795
|
(0, print_1.debugPrint)('-> Entered visitObject_members(..)');
|
|
769
|
-
|
|
770
|
-
|
|
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
|
+
}
|
|
799
|
+
const entries = {};
|
|
771
800
|
ctx.object_member_list().forEach((member) => {
|
|
772
801
|
const { key, value } = this.visitObject_member(member);
|
|
773
|
-
(
|
|
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
|
+
}
|
|
774
812
|
entries[key] = value;
|
|
775
813
|
});
|
|
776
|
-
(0, print_1.debugPrint)('<- About to exit visitObject_members(..)');
|
|
777
814
|
return entries;
|
|
778
815
|
};
|
|
779
816
|
/**
|
|
780
817
|
* Visit a parse tree produced by `YiniParser.object_member`.
|
|
781
818
|
* @param ctx the parse tree
|
|
782
|
-
* @grammarRule KEY
|
|
783
|
-
* @return the
|
|
819
|
+
* @grammarRule KEY object_member_separator NL* value
|
|
820
|
+
* @return the object member key and value
|
|
784
821
|
*/
|
|
785
|
-
// visitObject_member?: (ctx: Object_memberContext) => Result
|
|
786
822
|
this.visitObject_member = (ctx) => {
|
|
787
823
|
(0, print_1.debugPrint)('-> Entered visitObject_member(..)');
|
|
788
824
|
const rawKey = ctx.KEY().getText();
|
|
789
825
|
const key = (0, string_1.trimBackticks)(rawKey);
|
|
790
|
-
const
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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');
|
|
794
840
|
(0, print_1.debugPrint)(' rawKey = ' + rawKey);
|
|
795
841
|
(0, print_1.debugPrint)(' key = ' + key);
|
|
842
|
+
(0, print_1.debugPrint)('separator = ' + separator);
|
|
796
843
|
(0, print_1.debugPrint)('rawValue = ' + rawValue);
|
|
797
844
|
if (!valueNode) {
|
|
798
|
-
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
799
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 '-'.`);
|
|
800
846
|
}
|
|
801
847
|
(0, print_1.debugPrint)('<- About to exit visitObject_member(..)');
|
|
802
848
|
if ((0, env_1.isDebug)()) {
|
|
803
849
|
console.log('Returning:');
|
|
804
|
-
(0, print_1.printObject)({ key, value: valueNode });
|
|
850
|
+
(0, print_1.printObject)({ key, value: valueNode, separator });
|
|
805
851
|
}
|
|
806
|
-
return { key, value: valueNode };
|
|
852
|
+
return { key, value: valueNode, separator };
|
|
807
853
|
};
|
|
808
854
|
/**
|
|
809
|
-
* Visit a parse tree produced by `YiniParser.
|
|
855
|
+
* Visit a parse tree produced by `YiniParser.object_member_separator`.
|
|
810
856
|
* @param ctx the parse tree
|
|
811
|
-
* @grammarRule
|
|
812
|
-
* @return
|
|
857
|
+
* @grammarRule COLON | EQ
|
|
858
|
+
* @return ':' or '='
|
|
813
859
|
*/
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
(
|
|
817
|
-
|
|
818
|
-
(0, print_1.debugPrint)(`visitColon_list_decl(..): key = '${key}'`);
|
|
819
|
-
const elems = this.visitElements(ctx.elements());
|
|
820
|
-
const value = makeListValue(elems, 'From colon-list');
|
|
821
|
-
const current = this.sectionStack[this.sectionStack.length - 1];
|
|
822
|
-
// putMember(current, key, list, this.ast, this.onDuplicateKey)
|
|
823
|
-
this.putMember(this.errorHandler, ctx, current, key, value,
|
|
824
|
-
// this.ast,
|
|
825
|
-
this.onDuplicateKey);
|
|
826
|
-
(0, print_1.debugPrint)('<- About to exit visitColon_list_decl(..)...');
|
|
827
|
-
if ((0, env_1.isDebug)()) {
|
|
828
|
-
console.log('List literal: (from a Colon-list)');
|
|
829
|
-
(0, print_1.printObject)(value);
|
|
860
|
+
this.visitObject_member_separator = (ctx) => {
|
|
861
|
+
const sep = ctx.getText();
|
|
862
|
+
if (sep === ':' || sep === '=') {
|
|
863
|
+
return sep;
|
|
830
864
|
}
|
|
831
|
-
|
|
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 ':';
|
|
832
867
|
};
|
|
833
868
|
/**
|
|
834
|
-
*
|
|
835
|
-
* @
|
|
836
|
-
* @
|
|
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
|
|
837
876
|
*/
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const
|
|
841
|
-
const
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
let txt = ''
|
|
845
|
-
try {
|
|
846
|
-
txt = parseStringLiteral(parsedInput)
|
|
847
|
-
} catch (err) {
|
|
848
|
-
const msg = '' + (<any>err)?.message
|
|
849
|
-
this.errorHandler!.pushOrBail(
|
|
850
|
-
toErrorLocation(ctx),
|
|
851
|
-
'Syntax-Error',
|
|
852
|
-
'Parse error in string',
|
|
853
|
-
`${msg}`,
|
|
854
|
-
)
|
|
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.`);
|
|
855
883
|
}
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
const rawText = ctx.STRING().getText();
|
|
861
|
-
const parsedInput = this.extractStringKindAndValue(rawText);
|
|
862
|
-
try {
|
|
863
|
-
return (0, parseString_1.default)(parsedInput);
|
|
884
|
+
this.ast.yiniMarkerSeen = true;
|
|
885
|
+
this.meta_hasYiniMarker = true;
|
|
886
|
+
if (!modeText) {
|
|
887
|
+
return null;
|
|
864
888
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', '
|
|
868
|
-
return
|
|
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;
|
|
869
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();
|
|
870
907
|
};
|
|
871
908
|
/**
|
|
872
909
|
* Visit a parse tree produced by `YiniParser.bad_member`.
|
|
@@ -899,14 +936,10 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
899
936
|
this.options = options;
|
|
900
937
|
this.errorHandler = errorHandler;
|
|
901
938
|
this.isStrict = options?.rules?.initialMode === 'strict';
|
|
902
|
-
this.onDuplicateKey =
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
// } else {
|
|
906
|
-
// this.onDuplicateKey = 'warn'
|
|
907
|
-
// }
|
|
939
|
+
this.onDuplicateKey =
|
|
940
|
+
options?.rules?.onDuplicateKey ??
|
|
941
|
+
(this.isStrict ? 'error' : 'warn-and-keep-first');
|
|
908
942
|
const root = makeSection('(root)', 0);
|
|
909
|
-
// this.mapSectionNamePaths.set('(root)', 0)
|
|
910
943
|
this.ast = {
|
|
911
944
|
root,
|
|
912
945
|
isStrict: this.isStrict,
|
|
@@ -921,69 +954,37 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
921
954
|
};
|
|
922
955
|
this.sectionStack = [root];
|
|
923
956
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
// Triple quoted
|
|
934
|
-
if (rest.startsWith('"""')) {
|
|
935
|
-
const inner = rest.slice(3, -3);
|
|
936
|
-
if (prefix === 'C') {
|
|
937
|
-
return { strKind: 'triple-classic', value: inner };
|
|
938
|
-
}
|
|
939
|
-
return { strKind: 'triple-raw', value: inner };
|
|
957
|
+
// --- Private helper methods --------------------------------
|
|
958
|
+
validateStrictTopLevelStructure() {
|
|
959
|
+
if (!this.isStrict)
|
|
960
|
+
return;
|
|
961
|
+
const numTopLevelSections = this.ast.root.children.length;
|
|
962
|
+
const numTopLevelMembers = this.ast.root.members.size;
|
|
963
|
+
if (numTopLevelMembers > 0) {
|
|
964
|
+
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
965
|
+
this.errorHandler.pushOrBail(undefined, 'Syntax-Error', 'Top-level members are not allowed in strict mode.', 'Members were found outside the single required explicit top-level section.', 'Move all top-level members into the explicit top-level section, or parse the document in lenient mode.');
|
|
940
966
|
}
|
|
941
|
-
//
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
case 'C':
|
|
946
|
-
return { strKind: 'classic', value: inner };
|
|
947
|
-
case 'H':
|
|
948
|
-
return { strKind: 'hyper', value: inner };
|
|
949
|
-
case 'R':
|
|
950
|
-
case '':
|
|
951
|
-
return { strKind: 'raw', value: inner };
|
|
952
|
-
default:
|
|
953
|
-
return { strKind: 'raw', value: inner };
|
|
967
|
+
// Exactly one explicit top-level section in strict mode.
|
|
968
|
+
if (numTopLevelSections !== 1) {
|
|
969
|
+
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
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.');
|
|
954
971
|
}
|
|
955
972
|
}
|
|
956
973
|
extractStringKindAndValue(raw) {
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
prefix = 'C';
|
|
967
|
-
else if (/^[Hh]/.test(raw))
|
|
968
|
-
prefix = 'H';
|
|
969
|
-
else if (/^[Rr]/.test(raw))
|
|
970
|
-
prefix = 'R';
|
|
971
|
-
if (raw.startsWith('"""') ||
|
|
972
|
-
raw.startsWith('C"""') ||
|
|
973
|
-
raw.startsWith('c"""')) {
|
|
974
|
-
value = raw.replace(/^[CRcr]?"""/, '').replace(/"""$/, '');
|
|
975
|
-
strKind = prefix === 'C' ? 'triple-classic' : 'triple-raw';
|
|
976
|
-
}
|
|
977
|
-
else {
|
|
978
|
-
value = raw.replace(/^[CHRchr]?['"]/, '').replace(/['"]$/, '');
|
|
979
|
-
if (prefix === 'C')
|
|
980
|
-
strKind = 'classic';
|
|
981
|
-
else if (prefix === 'H')
|
|
982
|
-
strKind = 'hyper';
|
|
983
|
-
else
|
|
984
|
-
strKind = '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
|
+
};
|
|
985
983
|
}
|
|
986
|
-
return {
|
|
984
|
+
return {
|
|
985
|
+
strKind: hasClassicPrefix ? 'classic' : 'raw',
|
|
986
|
+
value: body.slice(1, -1),
|
|
987
|
+
};
|
|
987
988
|
}
|
|
988
989
|
/** Attach a section to the stack respecting up/down moves (Spec 5.3). :contentReference[oaicite:7]{index=7} */
|
|
989
990
|
attachSection(ctx, stack, section, ast) {
|
|
@@ -992,7 +993,7 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
992
993
|
if (targetLevel <= 0) {
|
|
993
994
|
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
994
995
|
this.errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Warning', `Invalid section level: ${targetLevel}`);
|
|
995
|
-
return;
|
|
996
|
+
return false;
|
|
996
997
|
}
|
|
997
998
|
// ------------------------------
|
|
998
999
|
// Construct section name path.
|
|
@@ -1008,12 +1009,15 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
1008
1009
|
(0, print_1.debugPrint)('section full path: keyPath = ' + keyPath);
|
|
1009
1010
|
// ------------------------------
|
|
1010
1011
|
if (this.hasDefinedSectionTitle(keyPath)) {
|
|
1011
|
-
|
|
1012
|
-
|
|
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;
|
|
1013
1017
|
}
|
|
1014
1018
|
else {
|
|
1015
1019
|
if (section.members === undefined) {
|
|
1016
|
-
(0, print_1.debugPrint)('This
|
|
1020
|
+
(0, print_1.debugPrint)('This section result does not hold any valid members (=undefined)');
|
|
1017
1021
|
}
|
|
1018
1022
|
else {
|
|
1019
1023
|
// this.existingSectionTitles.set(key, true)
|
|
@@ -1033,6 +1037,20 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
1033
1037
|
if (targetLevel > this.meta_maxLevel) {
|
|
1034
1038
|
this.meta_maxLevel = targetLevel;
|
|
1035
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;
|
|
1036
1054
|
}
|
|
1037
1055
|
/** Insert a key/value into current section (duplicate handling per options). */
|
|
1038
1056
|
putMember(errorHandler, ctx, sec, key, value, mode = 'warn-and-keep-first') {
|
|
@@ -1042,12 +1060,15 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
1042
1060
|
if (sec.members.has(key)) {
|
|
1043
1061
|
switch (mode) {
|
|
1044
1062
|
case 'error':
|
|
1063
|
+
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
1045
1064
|
errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Error', 'Hit a duplicate key in this section and scope', `Key '${key}' already exists in section '${sec.sectionName}' on level ${sec.level}.`);
|
|
1046
1065
|
break;
|
|
1047
1066
|
case 'warn-and-keep-first':
|
|
1067
|
+
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
1048
1068
|
errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Warning', `Hit a duplicate key (will keep first value) in this section and scope`, `Key '${key}' already exists in section '${sec.sectionName}' on level ${sec.level}.`);
|
|
1049
1069
|
return; // Keep first, don't overwrite.
|
|
1050
1070
|
case 'warn-and-overwrite':
|
|
1071
|
+
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
1051
1072
|
errorHandler.pushOrBail((0, errorDataHandler_1.toErrorLocation)(ctx), 'Syntax-Warning', `Overwrote a duplicate key (will keep last value) in this section and scope`, `Key '${key}' was overwritten in section '${sec.sectionName}' on level ${sec.level}.`);
|
|
1052
1073
|
break; // Overwrite, replace value.
|
|
1053
1074
|
case 'keep-first':
|
|
@@ -1061,34 +1082,76 @@ class ASTBuilder extends YiniParserVisitor_1.default {
|
|
|
1061
1082
|
}
|
|
1062
1083
|
sec.members.set(key, value);
|
|
1063
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
|
+
}
|
|
1064
1132
|
// --------------------------------
|
|
1065
1133
|
// Public entry
|
|
1066
1134
|
buildAST(ctx) {
|
|
1067
1135
|
this.visitYini?.(ctx);
|
|
1068
|
-
//
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1136
|
+
// Strict-mode structural validation.
|
|
1137
|
+
this.validateStrictTopLevelStructure();
|
|
1138
|
+
const isMissingTerminator = !this.ast.terminatorSeen;
|
|
1139
|
+
// In strict mode, the document terminator is required by default.
|
|
1140
|
+
// However, the parse option `requireDocTerminator` is authoritative
|
|
1141
|
+
// and may override that default behavior.
|
|
1142
|
+
const terminatorPolicy = this.options.rules.requireDocTerminator;
|
|
1143
|
+
if (isMissingTerminator && terminatorPolicy === 'required') {
|
|
1076
1144
|
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
1077
|
-
this.errorHandler.pushOrBail(undefined, 'Syntax-Error',
|
|
1145
|
+
this.errorHandler.pushOrBail(undefined, 'Syntax-Error', "Missing '/END' at end of document.", "The document terminator '/END' (case-insensitive) is required at the end of the document.", "Add '/END' as the final significant line, or change requireDocTerminator to 'optional' or 'warn-if-missing'.");
|
|
1078
1146
|
}
|
|
1079
|
-
else if (
|
|
1080
|
-
|
|
1081
|
-
const msgWhat = `Missing '/END' at end of document (option requireDocTerminator is ${this.options.rules.requireDocTerminator}).`;
|
|
1082
|
-
const msgWhy = `The terminator '/END' (case insensitive) might be missing at the end of the document.`;
|
|
1083
|
-
const msgHint = `This is option can be overriden by the option requireDocTerminator.`;
|
|
1147
|
+
else if (isMissingTerminator &&
|
|
1148
|
+
terminatorPolicy === 'warn-if-missing') {
|
|
1084
1149
|
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
|
|
1085
|
-
this.errorHandler.pushOrBail(undefined, 'Syntax-Warning',
|
|
1150
|
+
this.errorHandler.pushOrBail(undefined, 'Syntax-Warning', "Missing '/END' at end of document.", "The document terminator '/END' (case-insensitive) appears to be missing at the end of the document.", "Add '/END' as the final significant line, or change requireDocTerminator to 'optional'.");
|
|
1086
1151
|
}
|
|
1087
|
-
// Note: Below is important for error checking as well as for meta data.
|
|
1088
1152
|
this.ast.numOfSections = this.mapSectionNamePaths.size;
|
|
1089
1153
|
this.ast.numOfMembers = this._numOfMembers;
|
|
1090
1154
|
if (this.options.isIncludeMeta) {
|
|
1091
|
-
// Attach collected meta information.
|
|
1092
1155
|
this.ast.maxDepth = this.meta_maxLevel;
|
|
1093
1156
|
this.ast.sectionNamePaths = [...this.mapSectionNamePaths.keys()];
|
|
1094
1157
|
}
|