@atomic-ehr/fhirpath 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +716 -238
- package/dist/index.d.ts +225 -119
- package/dist/index.js +10911 -5600
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
- package/src/analyzer/augmentor.ts +242 -0
- package/src/analyzer/cursor-services.ts +75 -0
- package/src/analyzer/scope-manager.ts +57 -0
- package/src/analyzer/trivia-indexer.ts +58 -0
- package/src/analyzer/type-compat.ts +157 -0
- package/src/analyzer/utils.ts +132 -0
- package/src/analyzer.ts +921 -1208
- package/src/completion-provider.ts +209 -191
- package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
- package/src/complex-types/temporal.ts +1737 -0
- package/src/errors.ts +25 -3
- package/src/index.ts +17 -104
- package/src/inspect.ts +4 -4
- package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
- package/src/interpreter/navigator.ts +94 -0
- package/src/interpreter/runtime-context.ts +273 -0
- package/src/interpreter.ts +435 -469
- package/src/lexer.ts +188 -210
- package/src/model-provider.ts +71 -43
- package/src/operations/abs-function.ts +1 -1
- package/src/operations/aggregate-function.ts +84 -5
- package/src/operations/all-function.ts +4 -3
- package/src/operations/allFalse-function.ts +2 -1
- package/src/operations/allTrue-function.ts +2 -1
- package/src/operations/and-operator.ts +2 -1
- package/src/operations/anyFalse-function.ts +2 -1
- package/src/operations/anyTrue-function.ts +2 -1
- package/src/operations/as-function.ts +58 -0
- package/src/operations/as-operator.ts +57 -19
- package/src/operations/ceiling-function.ts +1 -1
- package/src/operations/children-function.ts +14 -5
- package/src/operations/combine-function.ts +6 -3
- package/src/operations/combine-operator.ts +6 -7
- package/src/operations/comparison.ts +692 -0
- package/src/operations/contains-function.ts +1 -1
- package/src/operations/contains-operator.ts +2 -1
- package/src/operations/convertsToBoolean-function.ts +78 -0
- package/src/operations/convertsToDecimal-function.ts +82 -0
- package/src/operations/convertsToInteger-function.ts +71 -0
- package/src/operations/convertsToLong-function.ts +89 -0
- package/src/operations/convertsToQuantity-function.ts +116 -0
- package/src/operations/convertsToString-function.ts +88 -0
- package/src/operations/count-function.ts +2 -1
- package/src/operations/dateOf-function.ts +69 -0
- package/src/operations/dayOf-function.ts +66 -0
- package/src/operations/decimal-boundaries.ts +133 -0
- package/src/operations/defineVariable-function.ts +130 -17
- package/src/operations/distinct-function.ts +1 -1
- package/src/operations/div-operator.ts +1 -1
- package/src/operations/divide-operator.ts +12 -7
- package/src/operations/dot-operator.ts +1 -1
- package/src/operations/empty-function.ts +30 -21
- package/src/operations/endsWith-function.ts +6 -1
- package/src/operations/equal-operator.ts +23 -32
- package/src/operations/equivalent-operator.ts +13 -53
- package/src/operations/exclude-function.ts +2 -1
- package/src/operations/exists-function.ts +4 -3
- package/src/operations/first-function.ts +1 -1
- package/src/operations/floor-function.ts +1 -1
- package/src/operations/greater-operator.ts +20 -3
- package/src/operations/greater-or-equal-operator.ts +20 -3
- package/src/operations/highBoundary-function.ts +120 -0
- package/src/operations/hourOf-function.ts +66 -0
- package/src/operations/iif-function.ts +186 -7
- package/src/operations/implies-operator.ts +1 -1
- package/src/operations/in-operator.ts +2 -1
- package/src/operations/index.ts +41 -0
- package/src/operations/indexOf-function.ts +1 -1
- package/src/operations/intersect-function.ts +1 -1
- package/src/operations/is-function.ts +59 -0
- package/src/operations/is-operator.ts +20 -9
- package/src/operations/isDistinct-function.ts +2 -1
- package/src/operations/join-function.ts +1 -1
- package/src/operations/last-function.ts +1 -1
- package/src/operations/lastIndexOf-function.ts +85 -0
- package/src/operations/length-function.ts +1 -1
- package/src/operations/less-operator.ts +20 -3
- package/src/operations/less-or-equal-operator.ts +20 -3
- package/src/operations/less-than.ts +2 -2
- package/src/operations/lowBoundary-function.ts +120 -0
- package/src/operations/lower-function.ts +1 -1
- package/src/operations/matches-function.ts +86 -0
- package/src/operations/matchesFull-function.ts +96 -0
- package/src/operations/millisecondOf-function.ts +66 -0
- package/src/operations/minus-operator.ts +69 -4
- package/src/operations/minuteOf-function.ts +66 -0
- package/src/operations/mod-operator.ts +1 -1
- package/src/operations/monthOf-function.ts +66 -0
- package/src/operations/multiply-operator.ts +27 -3
- package/src/operations/not-equal-operator.ts +24 -30
- package/src/operations/not-equivalent-operator.ts +13 -53
- package/src/operations/not-function.ts +1 -1
- package/src/operations/ofType-function.ts +8 -12
- package/src/operations/or-operator.ts +2 -1
- package/src/operations/plus-operator.ts +71 -7
- package/src/operations/power-function.ts +35 -10
- package/src/operations/repeat-function.ts +169 -0
- package/src/operations/replace-function.ts +1 -1
- package/src/operations/replaceMatches-function.ts +120 -0
- package/src/operations/round-function.ts +1 -1
- package/src/operations/secondOf-function.ts +66 -0
- package/src/operations/select-function.ts +66 -5
- package/src/operations/single-function.ts +1 -1
- package/src/operations/skip-function.ts +1 -1
- package/src/operations/split-function.ts +1 -1
- package/src/operations/sqrt-function.ts +15 -8
- package/src/operations/startsWith-function.ts +1 -1
- package/src/operations/subsetOf-function.ts +6 -2
- package/src/operations/substring-function.ts +1 -1
- package/src/operations/supersetOf-function.ts +6 -2
- package/src/operations/tail-function.ts +1 -1
- package/src/operations/take-function.ts +1 -1
- package/src/operations/temporal-functions.ts +555 -0
- package/src/operations/timeOf-function.ts +67 -0
- package/src/operations/timezoneOffsetOf-function.ts +69 -0
- package/src/operations/toBoolean-function.ts +27 -8
- package/src/operations/toChars-function.ts +56 -0
- package/src/operations/toDecimal-function.ts +27 -8
- package/src/operations/toInteger-function.ts +15 -3
- package/src/operations/toLong-function.ts +98 -0
- package/src/operations/toQuantity-function.ts +181 -0
- package/src/operations/toString-function.ts +45 -3
- package/src/operations/trace-function.ts +1 -1
- package/src/operations/trim-function.ts +1 -1
- package/src/operations/truncate-function.ts +1 -1
- package/src/operations/unary-minus-operator.ts +2 -2
- package/src/operations/unary-plus-operator.ts +1 -1
- package/src/operations/union-function.ts +1 -1
- package/src/operations/union-operator.ts +16 -26
- package/src/operations/upper-function.ts +1 -1
- package/src/operations/where-function.ts +3 -3
- package/src/operations/xor-operator.ts +1 -1
- package/src/operations/yearOf-function.ts +66 -0
- package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
- package/src/parser.ts +248 -501
- package/src/registry.ts +53 -42
- package/src/types.ts +128 -16
- package/src/utils/pprint.ts +151 -0
package/src/lexer.ts
CHANGED
|
@@ -23,6 +23,7 @@ export enum TokenType {
|
|
|
23
23
|
DATETIME = 4,
|
|
24
24
|
TIME = 5,
|
|
25
25
|
QUANTITY = 6, // Quantity literals like 5 'mg'
|
|
26
|
+
DATE = 7, // Date literals like @2020-01-01
|
|
26
27
|
|
|
27
28
|
// Operators (all symbol operators consolidated)
|
|
28
29
|
OPERATOR = 10, // +, -, *, /, <, >, <=, >=, =, !=, ~, !~, |, &
|
|
@@ -76,8 +77,6 @@ export class Lexer {
|
|
|
76
77
|
private position: number = 0;
|
|
77
78
|
private line: number = 1; // Legacy: 1-based for backward compatibility
|
|
78
79
|
private column: number = 1; // Legacy: 1-based for backward compatibility
|
|
79
|
-
private lspLine: number = 0; // LSP: zero-based
|
|
80
|
-
private lspCharacter: number = 0; // LSP: zero-based character within line
|
|
81
80
|
private options: LexerOptions;
|
|
82
81
|
private lineOffsets: number[] = [0]; // Start positions of each line
|
|
83
82
|
|
|
@@ -160,25 +159,13 @@ export class Lexer {
|
|
|
160
159
|
const start = this.position;
|
|
161
160
|
const startLine = this.line;
|
|
162
161
|
const startColumn = this.column;
|
|
163
|
-
const char = this.input[this.position]
|
|
162
|
+
const char = this.input[this.position]!;
|
|
164
163
|
const charCode = this.input.charCodeAt(this.position);
|
|
165
164
|
|
|
166
165
|
// Single character tokens
|
|
167
166
|
switch (char) {
|
|
168
|
-
case '+':
|
|
169
|
-
this.advance();
|
|
170
|
-
return this.createToken(TokenType.OPERATOR, '+', start, this.position, startLine, startColumn);
|
|
171
|
-
|
|
172
|
-
case '-':
|
|
173
|
-
this.advance();
|
|
174
|
-
return this.createToken(TokenType.OPERATOR, '-', start, this.position, startLine, startColumn);
|
|
175
|
-
|
|
176
|
-
case '*':
|
|
177
|
-
this.advance();
|
|
178
|
-
return this.createToken(TokenType.OPERATOR, '*', start, this.position, startLine, startColumn);
|
|
179
|
-
|
|
180
167
|
case '/':
|
|
181
|
-
//
|
|
168
|
+
// Handle comments first; otherwise treat as operator
|
|
182
169
|
if (this.peek() === '/') {
|
|
183
170
|
if (this.options.preserveTrivia) {
|
|
184
171
|
const commentStart = this.position;
|
|
@@ -217,53 +204,9 @@ export class Lexer {
|
|
|
217
204
|
return null;
|
|
218
205
|
}
|
|
219
206
|
}
|
|
220
|
-
|
|
221
|
-
return this.
|
|
222
|
-
|
|
223
|
-
case '<':
|
|
224
|
-
this.advance();
|
|
225
|
-
if (this.current() === '=') {
|
|
226
|
-
this.advance();
|
|
227
|
-
return this.createToken(TokenType.OPERATOR, '<=', start, this.position, startLine, startColumn);
|
|
228
|
-
}
|
|
229
|
-
return this.createToken(TokenType.OPERATOR, '<', start, this.position, startLine, startColumn);
|
|
230
|
-
|
|
231
|
-
case '>':
|
|
232
|
-
this.advance();
|
|
233
|
-
if (this.current() === '=') {
|
|
234
|
-
this.advance();
|
|
235
|
-
return this.createToken(TokenType.OPERATOR, '>=', start, this.position, startLine, startColumn);
|
|
236
|
-
}
|
|
237
|
-
return this.createToken(TokenType.OPERATOR, '>', start, this.position, startLine, startColumn);
|
|
238
|
-
|
|
239
|
-
case '=':
|
|
240
|
-
this.advance();
|
|
241
|
-
return this.createToken(TokenType.OPERATOR, '=', start, this.position, startLine, startColumn);
|
|
242
|
-
|
|
243
|
-
case '!':
|
|
244
|
-
this.advance();
|
|
245
|
-
if (this.current() === '=') {
|
|
246
|
-
this.advance();
|
|
247
|
-
return this.createToken(TokenType.OPERATOR, '!=', start, this.position, startLine, startColumn);
|
|
248
|
-
} else if (this.current() === '~') {
|
|
249
|
-
this.advance();
|
|
250
|
-
return this.createToken(TokenType.OPERATOR, '!~', start, this.position, startLine, startColumn);
|
|
251
|
-
}
|
|
252
|
-
// '!' alone is not a valid token in FHIRPath
|
|
253
|
-
throw this.error(`Unexpected character '!' at position ${start}`);
|
|
254
|
-
|
|
255
|
-
case '~':
|
|
256
|
-
this.advance();
|
|
257
|
-
return this.createToken(TokenType.OPERATOR, '~', start, this.position, startLine, startColumn);
|
|
258
|
-
|
|
259
|
-
case '|':
|
|
260
|
-
this.advance();
|
|
261
|
-
return this.createToken(TokenType.OPERATOR, '|', start, this.position, startLine, startColumn);
|
|
262
|
-
|
|
263
|
-
case '&':
|
|
264
|
-
this.advance();
|
|
265
|
-
return this.createToken(TokenType.OPERATOR, '&', start, this.position, startLine, startColumn);
|
|
266
|
-
|
|
207
|
+
// Fall through to operator matching after comment checks
|
|
208
|
+
return this.readOperator(start, startLine, startColumn);
|
|
209
|
+
|
|
267
210
|
case '.':
|
|
268
211
|
this.advance();
|
|
269
212
|
return this.createToken(TokenType.DOT, '.', start, this.position, startLine, startColumn);
|
|
@@ -315,7 +258,12 @@ export class Lexer {
|
|
|
315
258
|
case '$':
|
|
316
259
|
return this.readSpecialIdentifier();
|
|
317
260
|
}
|
|
318
|
-
|
|
261
|
+
|
|
262
|
+
// Greedy operator matching for other operator starters
|
|
263
|
+
if (this.isOperatorStarter(char)) {
|
|
264
|
+
return this.readOperator(start, startLine, startColumn);
|
|
265
|
+
}
|
|
266
|
+
|
|
319
267
|
// Numbers
|
|
320
268
|
if (charCode >= 48 && charCode <= 57) { // 0-9
|
|
321
269
|
return this.readNumber();
|
|
@@ -340,17 +288,7 @@ export class Lexer {
|
|
|
340
288
|
this.advance();
|
|
341
289
|
|
|
342
290
|
// Continue with alphanumeric or underscore
|
|
343
|
-
|
|
344
|
-
const charCode = this.input.charCodeAt(this.position);
|
|
345
|
-
if ((charCode >= 65 && charCode <= 90) || // A-Z
|
|
346
|
-
(charCode >= 97 && charCode <= 122) || // a-z
|
|
347
|
-
(charCode >= 48 && charCode <= 57) || // 0-9
|
|
348
|
-
charCode === 95) { // _
|
|
349
|
-
this.advance();
|
|
350
|
-
} else {
|
|
351
|
-
break;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
291
|
+
this.scanIdentifierBody();
|
|
354
292
|
|
|
355
293
|
const value = this.input.substring(start, this.position);
|
|
356
294
|
return this.createToken(TokenType.IDENTIFIER, value, start, this.position, startLine, startColumn);
|
|
@@ -361,28 +299,10 @@ export class Lexer {
|
|
|
361
299
|
const startLine = this.line;
|
|
362
300
|
const startColumn = this.column;
|
|
363
301
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
if (char === '`') {
|
|
370
|
-
this.advance(); // Skip closing `
|
|
371
|
-
const value = this.input.substring(start, this.position);
|
|
372
|
-
return this.createToken(TokenType.IDENTIFIER, value, start, this.position, startLine, startColumn);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (char === '\\') {
|
|
376
|
-
this.advance(); // Skip escape character
|
|
377
|
-
if (this.position >= this.input.length) {
|
|
378
|
-
throw this.error('Unterminated delimited identifier');
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
this.advance();
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
throw this.error('Unterminated delimited identifier');
|
|
302
|
+
// Current must be opening backtick
|
|
303
|
+
this.scanQuoted('`', 'Unterminated delimited identifier');
|
|
304
|
+
const value = this.input.substring(start, this.position);
|
|
305
|
+
return this.createToken(TokenType.IDENTIFIER, value, start, this.position, startLine, startColumn);
|
|
386
306
|
}
|
|
387
307
|
|
|
388
308
|
private readSpecialIdentifier(): Token {
|
|
@@ -392,18 +312,8 @@ export class Lexer {
|
|
|
392
312
|
|
|
393
313
|
this.advance(); // Skip $
|
|
394
314
|
|
|
395
|
-
// Read the identifier part
|
|
396
|
-
|
|
397
|
-
const charCode = this.input.charCodeAt(this.position);
|
|
398
|
-
if ((charCode >= 65 && charCode <= 90) || // A-Z
|
|
399
|
-
(charCode >= 97 && charCode <= 122) || // a-z
|
|
400
|
-
(charCode >= 48 && charCode <= 57) || // 0-9
|
|
401
|
-
charCode === 95) { // _
|
|
402
|
-
this.advance();
|
|
403
|
-
} else {
|
|
404
|
-
break;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
315
|
+
// Read the identifier part (may be empty)
|
|
316
|
+
this.scanIdentifierBody();
|
|
407
317
|
|
|
408
318
|
const value = this.input.substring(start, this.position);
|
|
409
319
|
|
|
@@ -423,108 +333,36 @@ export class Lexer {
|
|
|
423
333
|
|
|
424
334
|
if (char === '`') {
|
|
425
335
|
// Delimited identifier: %`identifier`
|
|
426
|
-
this.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const ch = this.current();
|
|
430
|
-
|
|
431
|
-
if (ch === '`') {
|
|
432
|
-
this.advance(); // Skip closing `
|
|
433
|
-
const value = this.input.substring(start, this.position);
|
|
434
|
-
return this.createToken(TokenType.ENVIRONMENT_VARIABLE, value, start, this.position, startLine, startColumn);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
if (ch === '\\') {
|
|
438
|
-
this.advance(); // Skip escape character
|
|
439
|
-
if (this.position >= this.input.length) {
|
|
440
|
-
throw this.error('Unterminated environment variable');
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
this.advance();
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
throw this.error('Unterminated environment variable');
|
|
336
|
+
this.scanQuoted('`', 'Unterminated environment variable');
|
|
337
|
+
const value = this.input.substring(start, this.position);
|
|
338
|
+
return this.createToken(TokenType.ENVIRONMENT_VARIABLE, value, start, this.position, startLine, startColumn);
|
|
448
339
|
} else if (char === "'") {
|
|
449
340
|
// String format (backwards compatibility): %'identifier'
|
|
450
|
-
this.
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const ch = this.current();
|
|
454
|
-
|
|
455
|
-
if (ch === "'") {
|
|
456
|
-
this.advance(); // Skip closing '
|
|
457
|
-
const value = this.input.substring(start, this.position);
|
|
458
|
-
return this.createToken(TokenType.ENVIRONMENT_VARIABLE, value, start, this.position, startLine, startColumn);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
if (ch === '\\') {
|
|
462
|
-
this.advance(); // Skip escape character
|
|
463
|
-
if (this.position >= this.input.length) {
|
|
464
|
-
throw this.error('Unterminated environment variable');
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
this.advance();
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
throw this.error('Unterminated environment variable');
|
|
341
|
+
this.scanQuoted("'", 'Unterminated environment variable');
|
|
342
|
+
const value = this.input.substring(start, this.position);
|
|
343
|
+
return this.createToken(TokenType.ENVIRONMENT_VARIABLE, value, start, this.position, startLine, startColumn);
|
|
472
344
|
} else {
|
|
473
345
|
// Simple identifier: %identifier
|
|
474
346
|
const charCode = this.input.charCodeAt(this.position);
|
|
475
|
-
if (!(
|
|
476
|
-
(charCode >= 97 && charCode <= 122) || // a-z
|
|
477
|
-
charCode === 95)) { // _
|
|
347
|
+
if (!this.isIdentifierHead(charCode)) {
|
|
478
348
|
throw this.error('Invalid environment variable name');
|
|
479
349
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const charCode = this.input.charCodeAt(this.position);
|
|
484
|
-
if ((charCode >= 65 && charCode <= 90) || // A-Z
|
|
485
|
-
(charCode >= 97 && charCode <= 122) || // a-z
|
|
486
|
-
(charCode >= 48 && charCode <= 57) || // 0-9
|
|
487
|
-
charCode === 95) { // _
|
|
488
|
-
this.advance();
|
|
489
|
-
} else {
|
|
490
|
-
break;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
350
|
+
// Consume head and the rest of identifier
|
|
351
|
+
this.advance();
|
|
352
|
+
this.scanIdentifierBody();
|
|
493
353
|
|
|
494
354
|
const value = this.input.substring(start, this.position);
|
|
495
355
|
return this.createToken(TokenType.ENVIRONMENT_VARIABLE, value, start, this.position, startLine, startColumn);
|
|
496
356
|
}
|
|
497
357
|
}
|
|
498
358
|
|
|
499
|
-
private readString(quote:
|
|
359
|
+
private readString(quote: "'" | '"'): Token {
|
|
500
360
|
const start = this.position;
|
|
501
361
|
const startLine = this.line;
|
|
502
362
|
const startColumn = this.column;
|
|
503
|
-
|
|
504
|
-
this.
|
|
505
|
-
|
|
506
|
-
while (this.position < this.input.length) {
|
|
507
|
-
const char = this.current();
|
|
508
|
-
|
|
509
|
-
if (char === quote) {
|
|
510
|
-
this.advance(); // Skip closing quote
|
|
511
|
-
const value = this.input.substring(start, this.position);
|
|
512
|
-
return this.createToken(TokenType.STRING, value, start, this.position, startLine, startColumn);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
if (char === '\\') {
|
|
516
|
-
this.advance(); // Skip escape character
|
|
517
|
-
if (this.position >= this.input.length) {
|
|
518
|
-
throw this.error('Unterminated string');
|
|
519
|
-
}
|
|
520
|
-
// Skip the escaped character
|
|
521
|
-
this.advance();
|
|
522
|
-
} else {
|
|
523
|
-
this.advance();
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
throw this.error('Unterminated string');
|
|
363
|
+
this.scanQuoted(quote, 'Unterminated string');
|
|
364
|
+
const value = this.input.substring(start, this.position);
|
|
365
|
+
return this.createToken(TokenType.STRING, value, start, this.position, startLine, startColumn);
|
|
528
366
|
}
|
|
529
367
|
|
|
530
368
|
private readNumber(): Token {
|
|
@@ -545,6 +383,31 @@ export class Lexer {
|
|
|
545
383
|
}
|
|
546
384
|
}
|
|
547
385
|
|
|
386
|
+
// Look ahead for a single-quoted unit to form a Quantity literal
|
|
387
|
+
// Pattern: <number>[ \t]?'<unit>' (no newline between number and unit)
|
|
388
|
+
const i = this.position;
|
|
389
|
+
let j = i;
|
|
390
|
+
// allow spaces/tabs only; do not cross lines
|
|
391
|
+
while (j < this.input.length) {
|
|
392
|
+
const ch = this.input[j]!;
|
|
393
|
+
if (ch === ' ' || ch === '\t') {
|
|
394
|
+
j++;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (j < this.input.length && this.input[j] === "'") {
|
|
401
|
+
// We will consume optional spaces/tabs and the quoted unit, then emit QUANTITY token
|
|
402
|
+
while (this.position < j) {
|
|
403
|
+
this.advance();
|
|
404
|
+
}
|
|
405
|
+
// Current is the opening quote; scan quoted unit including escapes
|
|
406
|
+
this.scanQuoted("'", 'Unterminated string');
|
|
407
|
+
const quantityValue = this.input.substring(start, this.position);
|
|
408
|
+
return this.createToken(TokenType.QUANTITY, quantityValue, start, this.position, startLine, startColumn);
|
|
409
|
+
}
|
|
410
|
+
|
|
548
411
|
const value = this.input.substring(start, this.position);
|
|
549
412
|
return this.createToken(TokenType.NUMBER, value, start, this.position, startLine, startColumn);
|
|
550
413
|
}
|
|
@@ -574,6 +437,8 @@ export class Lexer {
|
|
|
574
437
|
this.advance();
|
|
575
438
|
}
|
|
576
439
|
|
|
440
|
+
let hasTime = false;
|
|
441
|
+
|
|
577
442
|
// Optional month, day, time parts
|
|
578
443
|
if (this.current() === '-') {
|
|
579
444
|
this.advance();
|
|
@@ -597,17 +462,31 @@ export class Lexer {
|
|
|
597
462
|
}
|
|
598
463
|
}
|
|
599
464
|
|
|
600
|
-
//
|
|
465
|
+
// Check for time part or T suffix
|
|
601
466
|
if (this.current() === 'T') {
|
|
467
|
+
hasTime = true;
|
|
602
468
|
this.advance();
|
|
603
|
-
|
|
469
|
+
// Check if there's actual time content after T
|
|
470
|
+
if (this.isDigit(this.current())) {
|
|
471
|
+
this.readTimeFormat();
|
|
472
|
+
}
|
|
473
|
+
// else: it's just a T suffix like @2020T or @2020-01T
|
|
604
474
|
}
|
|
605
475
|
|
|
606
|
-
// Optional timezone
|
|
607
|
-
this.
|
|
476
|
+
// Optional timezone (only if we have time component with actual time values)
|
|
477
|
+
if (hasTime && this.position > 0 && this.input[this.position - 1] !== 'T') {
|
|
478
|
+
// Only read timezone if there's actual time content after T
|
|
479
|
+
this.readTimezone();
|
|
480
|
+
}
|
|
608
481
|
|
|
609
482
|
const value = this.input.substring(start, this.position);
|
|
610
|
-
|
|
483
|
+
|
|
484
|
+
// Determine token type based on content
|
|
485
|
+
// DateTime: has 'T' anywhere
|
|
486
|
+
// Date: no 'T' at all
|
|
487
|
+
const tokenType = hasTime ? TokenType.DATETIME : TokenType.DATE;
|
|
488
|
+
|
|
489
|
+
return this.createToken(tokenType, value, start, this.position, startLine, startColumn);
|
|
611
490
|
}
|
|
612
491
|
|
|
613
492
|
private readTime(start: number, startLine: number, startColumn: number): Token {
|
|
@@ -648,11 +527,11 @@ export class Lexer {
|
|
|
648
527
|
}
|
|
649
528
|
|
|
650
529
|
// Optional milliseconds
|
|
651
|
-
|
|
530
|
+
// Only consume the period if it's followed by a digit
|
|
531
|
+
// This prevents consuming periods that are method calls like .toDate()
|
|
532
|
+
if (this.current() === '.' && this.position + 1 < this.input.length &&
|
|
533
|
+
this.isDigit(this.input[this.position + 1]!)) {
|
|
652
534
|
this.advance();
|
|
653
|
-
if (!this.isDigit(this.current())) {
|
|
654
|
-
throw this.error('Invalid time format');
|
|
655
|
-
}
|
|
656
535
|
while (this.isDigit(this.current())) {
|
|
657
536
|
this.advance();
|
|
658
537
|
}
|
|
@@ -686,6 +565,82 @@ export class Lexer {
|
|
|
686
565
|
}
|
|
687
566
|
}
|
|
688
567
|
}
|
|
568
|
+
|
|
569
|
+
// Operator utilities
|
|
570
|
+
private static readonly OPERATORS: readonly string[] = [
|
|
571
|
+
'!=', '!~', '<=', '>=',
|
|
572
|
+
'+', '-', '*', '/', '<', '>', '=', '~', '|', '&',
|
|
573
|
+
];
|
|
574
|
+
|
|
575
|
+
private isOperatorStarter(ch: string): boolean {
|
|
576
|
+
switch (ch) {
|
|
577
|
+
case '+':
|
|
578
|
+
case '-':
|
|
579
|
+
case '*':
|
|
580
|
+
case '/':
|
|
581
|
+
case '<':
|
|
582
|
+
case '>':
|
|
583
|
+
case '=':
|
|
584
|
+
case '!':
|
|
585
|
+
case '~':
|
|
586
|
+
case '|':
|
|
587
|
+
case '&':
|
|
588
|
+
return true;
|
|
589
|
+
default:
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private readOperator(start: number, startLine: number, startColumn: number): Token {
|
|
595
|
+
// Try longest-first
|
|
596
|
+
for (const op of Lexer.OPERATORS) {
|
|
597
|
+
const end = start + op.length;
|
|
598
|
+
if (this.input.startsWith(op, start)) {
|
|
599
|
+
// Advance to end and emit
|
|
600
|
+
while (this.position < end) {
|
|
601
|
+
this.advance();
|
|
602
|
+
}
|
|
603
|
+
return this.createToken(TokenType.OPERATOR, op, start, this.position, startLine, startColumn);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// Special-case: lone '!'
|
|
607
|
+
if (this.input[start] === '!') {
|
|
608
|
+
// consume '!' to keep position consistent with old behavior before throwing?
|
|
609
|
+
// Old code threw without consuming, but tests only assert message content.
|
|
610
|
+
// Preserve position by not advancing.
|
|
611
|
+
throw this.error(`Unexpected character '!' at position ${start}`);
|
|
612
|
+
}
|
|
613
|
+
// Fallback: treat as unexpected
|
|
614
|
+
const ch = this.input[start] ?? '';
|
|
615
|
+
throw this.error(`Unexpected character '${ch}' at position ${start}`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Generic quoted scanner used by strings, delimited identifiers, env vars, and quantity units
|
|
619
|
+
private scanQuoted(quote: "'" | '"' | '`', unterminatedMessage: string): void {
|
|
620
|
+
if (this.current() !== quote) {
|
|
621
|
+
throw this.error('Internal error: scanQuoted called at non-quote');
|
|
622
|
+
}
|
|
623
|
+
// Skip opening quote
|
|
624
|
+
this.advance();
|
|
625
|
+
while (this.position < this.input.length) {
|
|
626
|
+
const ch = this.current();
|
|
627
|
+
if (ch === quote) {
|
|
628
|
+
this.advance(); // Skip closing quote
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (ch === '\\') {
|
|
632
|
+
this.advance();
|
|
633
|
+
if (this.position >= this.input.length) {
|
|
634
|
+
throw this.error(unterminatedMessage);
|
|
635
|
+
}
|
|
636
|
+
// Skip escaped character
|
|
637
|
+
this.advance();
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
this.advance();
|
|
641
|
+
}
|
|
642
|
+
throw this.error(unterminatedMessage);
|
|
643
|
+
}
|
|
689
644
|
|
|
690
645
|
private skipWhitespace(): void {
|
|
691
646
|
while (this.position < this.input.length) {
|
|
@@ -733,8 +688,6 @@ export class Lexer {
|
|
|
733
688
|
if (char === '\n') {
|
|
734
689
|
this.line++;
|
|
735
690
|
this.column = 1;
|
|
736
|
-
this.lspLine++;
|
|
737
|
-
this.lspCharacter = 0;
|
|
738
691
|
} else if (char === '\r') {
|
|
739
692
|
// Handle \r\n as single line ending
|
|
740
693
|
if (this.position + 1 < this.input.length && this.input[this.position + 1] === '\n') {
|
|
@@ -743,12 +696,9 @@ export class Lexer {
|
|
|
743
696
|
// Standalone \r
|
|
744
697
|
this.line++;
|
|
745
698
|
this.column = 1;
|
|
746
|
-
this.lspLine++;
|
|
747
|
-
this.lspCharacter = 0;
|
|
748
699
|
}
|
|
749
700
|
} else {
|
|
750
701
|
this.column++;
|
|
751
|
-
this.lspCharacter++;
|
|
752
702
|
}
|
|
753
703
|
}
|
|
754
704
|
this.position++;
|
|
@@ -773,6 +723,28 @@ export class Lexer {
|
|
|
773
723
|
if (!char) return false;
|
|
774
724
|
return char === ' ' || char === '\t' || char === '\n' || char === '\r';
|
|
775
725
|
}
|
|
726
|
+
|
|
727
|
+
// Identifier helpers (ASCII-based)
|
|
728
|
+
private isIdentifierHead(code: number): boolean {
|
|
729
|
+
return (code >= 65 && code <= 90) || // A-Z
|
|
730
|
+
(code >= 97 && code <= 122) || // a-z
|
|
731
|
+
code === 95; // _
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
private isIdentifierPart(code: number): boolean {
|
|
735
|
+
return this.isIdentifierHead(code) || (code >= 48 && code <= 57); // 0-9
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
private scanIdentifierBody(): void {
|
|
739
|
+
while (this.position < this.input.length) {
|
|
740
|
+
const code = this.input.charCodeAt(this.position);
|
|
741
|
+
if (this.isIdentifierPart(code)) {
|
|
742
|
+
this.advance();
|
|
743
|
+
} else {
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
776
748
|
|
|
777
749
|
/**
|
|
778
750
|
* Convert absolute offset to LSP Position
|
|
@@ -833,6 +805,12 @@ export class Lexer {
|
|
|
833
805
|
}
|
|
834
806
|
|
|
835
807
|
private error(message: string): Error {
|
|
808
|
+
if (this.options.trackPosition) {
|
|
809
|
+
const pos = this.offsetToPosition(this.position);
|
|
810
|
+
const line = pos.line + 1; // present as 1-based
|
|
811
|
+
const col = pos.character + 1; // present as 1-based
|
|
812
|
+
return new Error(`Lexer error at ${line}:${col}: ${message}`);
|
|
813
|
+
}
|
|
836
814
|
return new Error(`Lexer error: ${message}`);
|
|
837
815
|
}
|
|
838
816
|
|
|
@@ -878,4 +856,4 @@ export function isOperatorValue(token: Token, value: string): boolean {
|
|
|
878
856
|
// Helper to check if a token is an environment variable
|
|
879
857
|
export function isEnvironmentVariable(token: Token): boolean {
|
|
880
858
|
return token.type === TokenType.ENVIRONMENT_VARIABLE;
|
|
881
|
-
}
|
|
859
|
+
}
|