@atomic-ehr/fhirpath 0.0.1 → 0.0.2-canary.2fee5d9.20250808110507

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.
Files changed (101) hide show
  1. package/README.md +2 -0
  2. package/dist/index.d.ts +195 -77
  3. package/dist/index.js +2541 -981
  4. package/dist/index.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/analyzer.ts +536 -64
  7. package/src/boxing.ts +124 -0
  8. package/src/errors.ts +246 -0
  9. package/src/index.ts +57 -9
  10. package/src/inspect.ts +307 -174
  11. package/src/interpreter.ts +299 -48
  12. package/src/model-provider.ts +110 -8
  13. package/src/operations/abs-function.ts +19 -10
  14. package/src/operations/aggregate-function.ts +11 -3
  15. package/src/operations/all-function.ts +18 -8
  16. package/src/operations/allFalse-function.ts +9 -6
  17. package/src/operations/allTrue-function.ts +9 -6
  18. package/src/operations/and-operator.ts +20 -11
  19. package/src/operations/anyFalse-function.ts +6 -4
  20. package/src/operations/anyTrue-function.ts +6 -4
  21. package/src/operations/as-operator.ts +1 -0
  22. package/src/operations/ceiling-function.ts +9 -5
  23. package/src/operations/children-function.ts +94 -0
  24. package/src/operations/combine-function.ts +4 -2
  25. package/src/operations/combine-operator.ts +18 -4
  26. package/src/operations/contains-function.ts +20 -8
  27. package/src/operations/contains-operator.ts +14 -6
  28. package/src/operations/count-function.ts +2 -1
  29. package/src/operations/defineVariable-function.ts +5 -3
  30. package/src/operations/descendants-function.ts +62 -0
  31. package/src/operations/distinct-function.ts +16 -4
  32. package/src/operations/div-operator.ts +15 -2
  33. package/src/operations/divide-operator.ts +10 -5
  34. package/src/operations/dot-operator.ts +3 -1
  35. package/src/operations/empty-function.ts +2 -1
  36. package/src/operations/endsWith-function.ts +22 -10
  37. package/src/operations/equal-operator.ts +18 -6
  38. package/src/operations/equivalent-operator.ts +15 -10
  39. package/src/operations/exclude-function.ts +12 -10
  40. package/src/operations/exists-function.ts +14 -7
  41. package/src/operations/first-function.ts +7 -4
  42. package/src/operations/floor-function.ts +9 -5
  43. package/src/operations/greater-operator.ts +9 -4
  44. package/src/operations/greater-or-equal-operator.ts +9 -4
  45. package/src/operations/iif-function.ts +18 -16
  46. package/src/operations/implies-operator.ts +22 -7
  47. package/src/operations/in-operator.ts +17 -7
  48. package/src/operations/index.ts +3 -0
  49. package/src/operations/indexOf-function.ts +22 -10
  50. package/src/operations/intersect-function.ts +17 -20
  51. package/src/operations/is-operator.ts +63 -14
  52. package/src/operations/isDistinct-function.ts +12 -6
  53. package/src/operations/join-function.ts +15 -4
  54. package/src/operations/last-function.ts +7 -4
  55. package/src/operations/length-function.ts +12 -5
  56. package/src/operations/less-operator.ts +9 -4
  57. package/src/operations/less-or-equal-operator.ts +9 -4
  58. package/src/operations/less-than.ts +25 -1
  59. package/src/operations/lower-function.ts +12 -5
  60. package/src/operations/minus-operator.ts +9 -4
  61. package/src/operations/mod-operator.ts +19 -2
  62. package/src/operations/multiply-operator.ts +11 -6
  63. package/src/operations/not-equal-operator.ts +15 -6
  64. package/src/operations/not-equivalent-operator.ts +15 -10
  65. package/src/operations/not-function.ts +12 -4
  66. package/src/operations/ofType-function.ts +135 -0
  67. package/src/operations/or-operator.ts +20 -11
  68. package/src/operations/plus-operator.ts +18 -6
  69. package/src/operations/power-function.ts +21 -9
  70. package/src/operations/replace-function.ts +26 -9
  71. package/src/operations/round-function.ts +23 -8
  72. package/src/operations/select-function.ts +12 -6
  73. package/src/operations/single-function.ts +5 -3
  74. package/src/operations/skip-function.ts +12 -5
  75. package/src/operations/split-function.ts +24 -9
  76. package/src/operations/sqrt-function.ts +9 -5
  77. package/src/operations/startsWith-function.ts +20 -8
  78. package/src/operations/subsetOf-function.ts +14 -11
  79. package/src/operations/substring-function.ts +36 -19
  80. package/src/operations/supersetOf-function.ts +14 -11
  81. package/src/operations/tail-function.ts +3 -1
  82. package/src/operations/take-function.ts +12 -5
  83. package/src/operations/toBoolean-function.ts +18 -11
  84. package/src/operations/toDecimal-function.ts +13 -6
  85. package/src/operations/toInteger-function.ts +13 -6
  86. package/src/operations/toString-function.ts +17 -10
  87. package/src/operations/trace-function.ts +12 -5
  88. package/src/operations/trim-function.ts +11 -4
  89. package/src/operations/truncate-function.ts +9 -5
  90. package/src/operations/unary-minus-operator.ts +22 -12
  91. package/src/operations/unary-plus-operator.ts +1 -0
  92. package/src/operations/union-function.ts +19 -24
  93. package/src/operations/union-operator.ts +1 -0
  94. package/src/operations/upper-function.ts +12 -5
  95. package/src/operations/where-function.ts +15 -8
  96. package/src/operations/xor-operator.ts +8 -3
  97. package/src/parser.ts +391 -8
  98. package/src/quantity-value.ts +4 -8
  99. package/src/registry.ts +3 -3
  100. package/src/types.ts +10 -6
  101. package/src/parser-base.ts +0 -400
package/src/analyzer.ts CHANGED
@@ -20,26 +20,15 @@ import type {
20
20
  } from './types';
21
21
  import { NodeType, DiagnosticSeverity } from './types';
22
22
  import { registry } from './registry';
23
+ import { Errors, toDiagnostic } from './errors';
23
24
 
24
- // Diagnostic codes as constants to avoid magic strings
25
- const DiagnosticCode = {
26
- UnknownOperator: 'UNKNOWN_OPERATOR',
27
- UnknownVariable: 'UNKNOWN_VARIABLE',
28
- UnknownUserVariable: 'UNKNOWN_USER_VARIABLE',
29
- UnknownFunction: 'UNKNOWN_FUNCTION',
30
- TooFewArgs: 'TOO_FEW_ARGS',
31
- TooManyArgs: 'TOO_MANY_ARGS',
32
- TypeMismatch: 'TYPE_MISMATCH',
33
- InputTypeMismatch: 'INPUT_TYPE_MISMATCH',
34
- ArgumentTypeMismatch: 'ARGUMENT_TYPE_MISMATCH',
35
- ParseError: 'PARSE_ERROR',
36
- } as const;
37
25
 
38
26
  export class Analyzer {
39
27
  private diagnostics: Diagnostic[] = [];
40
- private variables: Set<string> = new Set(['$this', '$index', '$total']);
28
+ private variables: Set<string> = new Set(['$this', '$index', '$total', 'context', 'resource', 'rootResource']);
41
29
  private modelProvider?: ModelProvider;
42
30
  private userVariableTypes: Map<string, TypeInfo> = new Map();
31
+ private systemVariableTypes: Map<string, TypeInfo> = new Map();
43
32
 
44
33
  constructor(modelProvider?: ModelProvider) {
45
34
  this.modelProvider = modelProvider;
@@ -101,10 +90,10 @@ export class Analyzer {
101
90
  this.visitNode((node as UnaryNode).operand);
102
91
  break;
103
92
  case NodeType.MembershipTest:
104
- this.visitNode((node as MembershipTestNode).expression);
93
+ this.visitMembershipTest(node as MembershipTestNode);
105
94
  break;
106
95
  case NodeType.TypeCast:
107
- this.visitNode((node as TypeCastNode).expression);
96
+ this.visitTypeCast(node as TypeCastNode);
108
97
  break;
109
98
  case NodeType.Variable:
110
99
  this.validateVariable((node as VariableNode).name, node);
@@ -120,6 +109,32 @@ export class Analyzer {
120
109
  private visitBinaryOperator(node: BinaryNode): void {
121
110
  this.visitNode(node.left);
122
111
 
112
+ // Track defineVariable for validation - collect all variables defined in the chain
113
+ if (node.operator === '.') {
114
+ const definedVars = this.collectDefinedVariables(node.left);
115
+ if (definedVars.size > 0) {
116
+ // Track which variables were already known
117
+ const previouslyKnown = new Set<string>();
118
+ definedVars.forEach(varName => {
119
+ if (this.variables.has(varName)) {
120
+ previouslyKnown.add(varName);
121
+ }
122
+ this.variables.add(varName);
123
+ });
124
+
125
+ // Visit right side with new variables in scope
126
+ this.visitNode(node.right);
127
+
128
+ // Restore previous state
129
+ definedVars.forEach(varName => {
130
+ if (!previouslyKnown.has(varName)) {
131
+ this.variables.delete(varName);
132
+ }
133
+ });
134
+ return;
135
+ }
136
+ }
137
+
123
138
  // Special handling for dot operator with function on right side
124
139
  if (node.operator === '.' && node.right.type === NodeType.Function) {
125
140
  const funcNode = node.right as FunctionNode;
@@ -130,11 +145,8 @@ export class Analyzer {
130
145
  if (!this.isTypeCompatible(node.left.typeInfo, func.signature.input)) {
131
146
  const inputTypeStr = this.typeToString(node.left.typeInfo);
132
147
  const expectedTypeStr = this.typeToString(func.signature.input);
133
- this.addDiagnostic(
134
- DiagnosticSeverity.Error,
135
- `Type mismatch: function '${func.name}' expects input type ${expectedTypeStr} but got ${inputTypeStr}`,
136
- funcNode,
137
- DiagnosticCode.InputTypeMismatch
148
+ this.diagnostics.push(
149
+ toDiagnostic(Errors.typeNotAssignable(inputTypeStr, expectedTypeStr, funcNode.range))
138
150
  );
139
151
  }
140
152
  }
@@ -150,7 +162,9 @@ export class Analyzer {
150
162
 
151
163
  const op = registry.getOperatorDefinition(node.operator);
152
164
  if (!op) {
153
- this.addDiagnostic(DiagnosticSeverity.Error, `Unknown operator: ${node.operator}`, node, DiagnosticCode.UnknownOperator);
165
+ this.diagnostics.push(
166
+ toDiagnostic(Errors.unknownOperator(node.operator, node.range))
167
+ );
154
168
  return;
155
169
  }
156
170
 
@@ -167,10 +181,73 @@ export class Analyzer {
167
181
  private visitFunctionCall(node: FunctionNode): void {
168
182
  if (node.name.type === NodeType.Identifier) {
169
183
  const funcName = (node.name as IdentifierNode).name;
184
+
185
+ // Check if this is a type operation that requires ModelProvider
186
+ if (funcName === 'ofType' && !this.modelProvider) {
187
+ // Check if the type argument is a primitive type
188
+ const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
189
+ let isPrimitive = false;
190
+
191
+ if (node.arguments.length > 0) {
192
+ const typeArg = node.arguments[0]!;
193
+ if (typeArg.type === NodeType.Identifier) {
194
+ isPrimitive = primitiveTypes.includes((typeArg as IdentifierNode).name);
195
+ } else if ((typeArg as any).type === NodeType.TypeOrIdentifier || (typeArg as any).type === NodeType.TypeReference) {
196
+ isPrimitive = primitiveTypes.includes((typeArg as any).name);
197
+ }
198
+ }
199
+
200
+ if (!isPrimitive) {
201
+ this.diagnostics.push(
202
+ toDiagnostic(Errors.modelProviderRequired('ofType', node.range))
203
+ );
204
+ }
205
+ }
206
+
207
+ // Check ofType with union types
208
+ if (funcName === 'ofType' && node.typeInfo) {
209
+ const inputType = node.typeInfo;
210
+ if (node.arguments.length > 0 && inputType.modelContext &&
211
+ typeof inputType.modelContext === 'object' &&
212
+ 'isUnion' in inputType.modelContext &&
213
+ inputType.modelContext.isUnion &&
214
+ 'choices' in inputType.modelContext &&
215
+ Array.isArray(inputType.modelContext.choices)) {
216
+
217
+ // Extract target type from argument
218
+ let targetType: string | undefined;
219
+ const typeArg = node.arguments[0]!;
220
+ if (typeArg.type === NodeType.Identifier) {
221
+ targetType = (typeArg as IdentifierNode).name;
222
+ } else if ((typeArg as any).type === NodeType.TypeOrIdentifier || (typeArg as any).type === NodeType.TypeReference) {
223
+ targetType = (typeArg as any).name;
224
+ }
225
+
226
+ if (targetType) {
227
+ const validChoice = inputType.modelContext.choices.find((choice: any) =>
228
+ choice.type === targetType || choice.code === targetType
229
+ );
230
+
231
+ if (!validChoice) {
232
+ this.diagnostics.push({
233
+ severity: DiagnosticSeverity.Warning,
234
+ code: 'invalid-type-filter',
235
+ message: `Type '${targetType}' is not present in the union type. Available types: ${
236
+ inputType.modelContext.choices.map((c: any) => c.type || c.code).join(', ')
237
+ }`,
238
+ range: node.range
239
+ });
240
+ }
241
+ }
242
+ }
243
+ }
244
+
170
245
  const func = registry.getFunction(funcName);
171
246
 
172
247
  if (!func) {
173
- this.addDiagnostic(DiagnosticSeverity.Error, `Unknown function: ${funcName}`, node, DiagnosticCode.UnknownFunction);
248
+ this.diagnostics.push(
249
+ toDiagnostic(Errors.unknownFunction(funcName, node.range))
250
+ );
174
251
  } else {
175
252
  // Check argument count based on signature
176
253
  const params = func.signature.parameters;
@@ -178,9 +255,13 @@ export class Analyzer {
178
255
  const maxParams = params.length;
179
256
 
180
257
  if (node.arguments.length < requiredParams) {
181
- this.addDiagnostic(DiagnosticSeverity.Error, `Function '${funcName}' requires at least ${requiredParams} arguments, got ${node.arguments.length}`, node, DiagnosticCode.TooFewArgs);
258
+ this.diagnostics.push(
259
+ toDiagnostic(Errors.wrongArgumentCount(funcName, requiredParams, node.arguments.length, node.range))
260
+ );
182
261
  } else if (node.arguments.length > maxParams) {
183
- this.addDiagnostic(DiagnosticSeverity.Error, `Function '${funcName}' accepts at most ${maxParams} arguments, got ${node.arguments.length}`, node, DiagnosticCode.TooManyArgs);
262
+ this.diagnostics.push(
263
+ toDiagnostic(Errors.wrongArgumentCount(funcName, maxParams, node.arguments.length, node.range))
264
+ );
184
265
  }
185
266
 
186
267
  // Type check arguments if we have type information
@@ -193,30 +274,215 @@ export class Analyzer {
193
274
  node.arguments.forEach(arg => this.visitNode(arg));
194
275
  }
195
276
 
277
+ private visitMembershipTest(node: MembershipTestNode): void {
278
+ // Check if ModelProvider is required
279
+ // Basic primitive types can be checked without ModelProvider
280
+ const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
281
+ if (!this.modelProvider && !primitiveTypes.includes(node.targetType)) {
282
+ this.diagnostics.push(
283
+ toDiagnostic(Errors.modelProviderRequired('is', node.range))
284
+ );
285
+ }
286
+
287
+ // Check 'is' with union types
288
+ if (node.expression.typeInfo) {
289
+ const leftType = node.expression.typeInfo;
290
+ if (leftType.modelContext &&
291
+ typeof leftType.modelContext === 'object' &&
292
+ 'isUnion' in leftType.modelContext &&
293
+ leftType.modelContext.isUnion &&
294
+ 'choices' in leftType.modelContext &&
295
+ Array.isArray(leftType.modelContext.choices)) {
296
+
297
+ const targetTypeName = node.targetType;
298
+ const validChoice = leftType.modelContext.choices.find((choice: any) =>
299
+ choice.type === targetTypeName || choice.code === targetTypeName
300
+ );
301
+
302
+ if (!validChoice) {
303
+ this.diagnostics.push({
304
+ severity: DiagnosticSeverity.Warning,
305
+ code: 'invalid-type-test',
306
+ message: `Type test 'is ${targetTypeName}' will always be false. Type '${targetTypeName}' is not in the union. Available types: ${
307
+ leftType.modelContext.choices.map((c: any) => c.type || c.code).join(', ')
308
+ }`,
309
+ range: node.range
310
+ });
311
+ }
312
+ }
313
+ }
314
+
315
+ this.visitNode(node.expression);
316
+ }
317
+
318
+ private visitTypeCast(node: TypeCastNode): void {
319
+ // Check if ModelProvider is required
320
+ // Basic primitive types can be checked without ModelProvider
321
+ const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
322
+ if (!this.modelProvider && !primitiveTypes.includes(node.targetType)) {
323
+ this.diagnostics.push(
324
+ toDiagnostic(Errors.modelProviderRequired('as', node.range))
325
+ );
326
+ }
327
+
328
+ // Check 'as' with union types
329
+ if (node.expression.typeInfo) {
330
+ const leftType = node.expression.typeInfo;
331
+ if (leftType.modelContext &&
332
+ typeof leftType.modelContext === 'object' &&
333
+ 'isUnion' in leftType.modelContext &&
334
+ leftType.modelContext.isUnion &&
335
+ 'choices' in leftType.modelContext &&
336
+ Array.isArray(leftType.modelContext.choices)) {
337
+
338
+ const targetTypeName = node.targetType;
339
+ const validChoice = leftType.modelContext.choices.find((choice: any) =>
340
+ choice.type === targetTypeName || choice.code === targetTypeName
341
+ );
342
+
343
+ if (!validChoice) {
344
+ this.diagnostics.push({
345
+ severity: DiagnosticSeverity.Warning,
346
+ code: 'invalid-type-cast',
347
+ message: `Type cast 'as ${targetTypeName}' may fail. Type '${targetTypeName}' is not guaranteed in the union. Available types: ${
348
+ leftType.modelContext.choices.map((c: any) => c.type || c.code).join(', ')
349
+ }`,
350
+ range: node.range
351
+ });
352
+ }
353
+ }
354
+ }
355
+
356
+ this.visitNode(node.expression);
357
+ }
358
+
196
359
  // Unified variable validation to eliminate duplication
197
360
  private validateVariable(name: string, node: ASTNode): void {
198
361
  if (name.startsWith('$')) {
199
362
  if (!this.variables.has(name)) {
200
- this.addDiagnostic(DiagnosticSeverity.Error, `Unknown variable: ${name}`, node, DiagnosticCode.UnknownVariable);
363
+ this.diagnostics.push(
364
+ toDiagnostic(Errors.unknownVariable(name, node.range))
365
+ );
201
366
  }
202
367
  } else if (name.startsWith('%')) {
203
368
  const varName = name.substring(1);
204
369
  if (!this.variables.has(varName)) {
205
- this.addDiagnostic(DiagnosticSeverity.Error, `Unknown user variable: ${name}`, node, DiagnosticCode.UnknownUserVariable);
370
+ this.diagnostics.push(
371
+ toDiagnostic(Errors.unknownUserVariable(name, node.range))
372
+ );
206
373
  }
207
374
  }
208
375
  }
209
-
210
- private addDiagnostic(severity: DiagnosticSeverity, message: string, node: ASTNode, code: string): void {
211
- this.diagnostics.push({
212
- range: node.range,
213
- severity,
214
- message,
215
- code,
216
- source: 'fhirpath-analyzer'
217
- });
376
+
377
+ private collectDefinedVariables(node: ASTNode): Set<string> {
378
+ const vars = new Set<string>();
379
+
380
+ // If this is a defineVariable call, extract the variable name
381
+ if (node.type === NodeType.Function) {
382
+ const funcNode = node as FunctionNode;
383
+ if (funcNode.name.type === NodeType.Identifier &&
384
+ (funcNode.name as IdentifierNode).name === 'defineVariable' &&
385
+ funcNode.arguments.length >= 1) {
386
+ const nameArg = funcNode.arguments[0];
387
+ if (nameArg && nameArg.type === NodeType.Literal && nameArg.valueType === 'string') {
388
+ vars.add(nameArg.value as string);
389
+ }
390
+ }
391
+ }
392
+
393
+ // If this is a binary dot operator, collect from left side recursively
394
+ if (node.type === NodeType.Binary) {
395
+ const binaryNode = node as BinaryNode;
396
+ if (binaryNode.operator === '.') {
397
+ // Collect from left side
398
+ const leftVars = this.collectDefinedVariables(binaryNode.left);
399
+ leftVars.forEach(v => vars.add(v));
400
+
401
+ // Check if right side is also defineVariable
402
+ if (binaryNode.right.type === NodeType.Function) {
403
+ const rightFunc = binaryNode.right as FunctionNode;
404
+ if (rightFunc.name.type === NodeType.Identifier &&
405
+ (rightFunc.name as IdentifierNode).name === 'defineVariable' &&
406
+ rightFunc.arguments.length >= 1) {
407
+ const nameArg = rightFunc.arguments[0];
408
+ if (nameArg && nameArg.type === NodeType.Literal && nameArg.valueType === 'string') {
409
+ vars.add(nameArg.value as string);
410
+ }
411
+ }
412
+ }
413
+ }
414
+ }
415
+
416
+ return vars;
417
+ }
418
+
419
+ private collectDefinedVariablesWithTypes(node: ASTNode): Map<string, TypeInfo> {
420
+ const varsWithTypes = new Map<string, TypeInfo>();
421
+
422
+ // If this is a defineVariable call, extract the variable name and type
423
+ if (node.type === NodeType.Function) {
424
+ const funcNode = node as FunctionNode;
425
+ if (funcNode.name.type === NodeType.Identifier &&
426
+ (funcNode.name as IdentifierNode).name === 'defineVariable' &&
427
+ funcNode.arguments.length >= 1) {
428
+ const nameArg = funcNode.arguments[0];
429
+ if (nameArg && nameArg.type === NodeType.Literal && nameArg.valueType === 'string') {
430
+ const varName = nameArg.value as string;
431
+ let varType: TypeInfo;
432
+
433
+ if (funcNode.arguments.length >= 2 && funcNode.arguments[1]!.typeInfo) {
434
+ // Has value expression - use its type
435
+ varType = funcNode.arguments[1]!.typeInfo;
436
+ } else if (node.typeInfo) {
437
+ // No value expression - uses input as value (defineVariable returns input)
438
+ varType = node.typeInfo;
439
+ } else {
440
+ varType = { type: 'Any', singleton: false };
441
+ }
442
+
443
+ varsWithTypes.set(varName, varType);
444
+ }
445
+ }
446
+ }
447
+
448
+ // If this is a binary dot operator, collect from entire chain
449
+ if (node.type === NodeType.Binary) {
450
+ const binaryNode = node as BinaryNode;
451
+ if (binaryNode.operator === '.') {
452
+ // Collect from left side recursively
453
+ const leftVars = this.collectDefinedVariablesWithTypes(binaryNode.left);
454
+ leftVars.forEach((type, name) => varsWithTypes.set(name, type));
455
+
456
+ // Check if right side is also defineVariable
457
+ if (binaryNode.right.type === NodeType.Function) {
458
+ const rightFunc = binaryNode.right as FunctionNode;
459
+ if (rightFunc.name.type === NodeType.Identifier &&
460
+ (rightFunc.name as IdentifierNode).name === 'defineVariable' &&
461
+ rightFunc.arguments.length >= 1) {
462
+ const nameArg = rightFunc.arguments[0];
463
+ if (nameArg && nameArg.type === NodeType.Literal && nameArg.valueType === 'string') {
464
+ const varName = nameArg.value as string;
465
+ let varType: TypeInfo;
466
+
467
+ if (rightFunc.arguments.length >= 2 && rightFunc.arguments[1]!.typeInfo) {
468
+ varType = rightFunc.arguments[1]!.typeInfo;
469
+ } else if (binaryNode.typeInfo) {
470
+ varType = binaryNode.typeInfo;
471
+ } else {
472
+ varType = { type: 'Any', singleton: false };
473
+ }
474
+
475
+ varsWithTypes.set(varName, varType);
476
+ }
477
+ }
478
+ }
479
+ }
480
+ }
481
+
482
+ return varsWithTypes;
218
483
  }
219
484
 
485
+
220
486
  // Type inference methods
221
487
  private inferType(node: ASTNode, inputType?: TypeInfo): TypeInfo {
222
488
  // Handle error nodes
@@ -341,11 +607,8 @@ export class Analyzer {
341
607
  // Skip diagnostics for union types - they may have dynamic properties
342
608
  if (leftType.namespace && leftType.name && leftType.modelContext &&
343
609
  !(leftType.modelContext as any).isUnion) {
344
- this.addDiagnostic(
345
- DiagnosticSeverity.Error,
346
- `Unknown property '${propertyName}' on type ${leftType.namespace}.${leftType.name}`,
347
- node.right,
348
- 'UNKNOWN_PROPERTY'
610
+ this.diagnostics.push(
611
+ toDiagnostic(Errors.unknownProperty(propertyName, `${leftType.namespace}.${leftType.name}`, node.right.range))
349
612
  );
350
613
  }
351
614
  }
@@ -381,6 +644,81 @@ export class Analyzer {
381
644
  return { type: 'Any', singleton: false };
382
645
  }
383
646
 
647
+ // Special handling for iif function
648
+ if (funcName === 'iif') {
649
+ // iif returns the common type of the true and false branches
650
+ if (node.arguments.length >= 2) {
651
+ const trueBranchType = this.inferType(node.arguments[1]!, inputType);
652
+ if (node.arguments.length >= 3) {
653
+ const falseBranchType = this.inferType(node.arguments[2]!, inputType);
654
+ // If both branches have the same type, use that
655
+ if (trueBranchType.type === falseBranchType.type &&
656
+ trueBranchType.singleton === falseBranchType.singleton) {
657
+ return trueBranchType;
658
+ }
659
+ // If types are the same but singleton differs, return as collection
660
+ if (trueBranchType.type === falseBranchType.type) {
661
+ // One is singleton, one is collection - result must be collection
662
+ return { type: trueBranchType.type, singleton: false };
663
+ }
664
+ // Otherwise, check if one is a subtype of the other
665
+ if (this.isTypeCompatible(trueBranchType, falseBranchType)) {
666
+ return falseBranchType;
667
+ }
668
+ if (this.isTypeCompatible(falseBranchType, trueBranchType)) {
669
+ return trueBranchType;
670
+ }
671
+ } else {
672
+ // Only true branch, result can be that type or empty
673
+ return { ...trueBranchType, singleton: false };
674
+ }
675
+ }
676
+ return { type: 'Any', singleton: false };
677
+ }
678
+
679
+ // Special handling for defineVariable function
680
+ if (funcName === 'defineVariable') {
681
+ // defineVariable returns its input type unchanged
682
+ return inputType || { type: 'Any', singleton: false };
683
+ }
684
+
685
+ // Special handling for aggregate function
686
+ if (funcName === 'aggregate') {
687
+ // If init parameter is provided, use its type to infer result type
688
+ if (node.arguments.length >= 2) {
689
+ const initType = this.inferType(node.arguments[1]!, inputType);
690
+ // The result type is the same as init type
691
+ return initType;
692
+ }
693
+ // Without init, we can't fully infer the type without running annotation
694
+ // This is a limitation - the actual type will be set during annotateAST
695
+ if (node.arguments.length >= 1) {
696
+ // We could try to infer, but it would require setting up system variables
697
+ // For now, return Any and let annotateAST handle proper typing
698
+ return { type: 'Any', singleton: false };
699
+ }
700
+ // No arguments at all
701
+ return { type: 'Any', singleton: false };
702
+ }
703
+
704
+ // Special handling for children function
705
+ if (funcName === 'children') {
706
+ if (inputType && this.modelProvider && 'getChildrenType' in this.modelProvider) {
707
+ const childrenType = this.modelProvider.getChildrenType(inputType);
708
+ if (childrenType) {
709
+ return childrenType;
710
+ }
711
+ }
712
+ // Fallback to Any collection
713
+ return { type: 'Any', singleton: false };
714
+ }
715
+
716
+ // Special handling for descendants function
717
+ // Returns Any type due to combinatorial explosion of possible types
718
+ if (funcName === 'descendants') {
719
+ return { type: 'Any', singleton: false };
720
+ }
721
+
384
722
  // Special handling for functions with dynamic result types
385
723
  if (func.signature.result === 'inputType') {
386
724
  // Functions like where() return the same type as input but always as collection
@@ -443,11 +781,28 @@ export class Analyzer {
443
781
  }
444
782
 
445
783
  private inferVariableType(node: VariableNode): TypeInfo {
446
- // Built-in variables
447
- if (node.name === '$this') {
448
- return { type: 'Any', singleton: false }; // $this is usually a collection
449
- } else if (node.name === '$index' || node.name === '$total') {
450
- return { type: 'Integer', singleton: true };
784
+ // System variables - check temporary context
785
+ if (node.name.startsWith('$')) {
786
+ const systemType = this.systemVariableTypes.get(node.name);
787
+ if (systemType) {
788
+ return systemType;
789
+ }
790
+ // Fallback defaults for system variables
791
+ switch (node.name) {
792
+ case '$this':
793
+ return { type: 'Any', singleton: false };
794
+ case '$index':
795
+ return { type: 'Integer', singleton: true };
796
+ case '$total':
797
+ return { type: 'Any', singleton: false };
798
+ default:
799
+ return { type: 'Any', singleton: false };
800
+ }
801
+ }
802
+
803
+ // Special FHIRPath environment variables
804
+ if (node.name === '%context' || node.name === '%resource' || node.name === '%rootResource') {
805
+ return { type: 'Any', singleton: false }; // These return the original input
451
806
  }
452
807
 
453
808
  // User-defined variables - check with or without % prefix
@@ -626,11 +981,8 @@ export class Analyzer {
626
981
  if (!foundMatch) {
627
982
  const leftTypeStr = this.typeToString(leftType);
628
983
  const rightTypeStr = this.typeToString(rightType);
629
- this.addDiagnostic(
630
- DiagnosticSeverity.Error,
631
- `Type mismatch: operator '${node.operator}' cannot be applied to types ${leftTypeStr} and ${rightTypeStr}`,
632
- node,
633
- DiagnosticCode.TypeMismatch
984
+ this.diagnostics.push(
985
+ toDiagnostic(Errors.operatorTypeMismatch(node.operator, leftTypeStr, rightTypeStr, node.range))
634
986
  );
635
987
  }
636
988
  }
@@ -647,11 +999,8 @@ export class Analyzer {
647
999
  if (!this.isTypeCompatible(arg.typeInfo, param.type)) {
648
1000
  const argTypeStr = this.typeToString(arg.typeInfo);
649
1001
  const paramTypeStr = this.typeToString(param.type);
650
- this.addDiagnostic(
651
- DiagnosticSeverity.Error,
652
- `Type mismatch: argument ${i + 1} of function '${func.name}' expects ${paramTypeStr} but got ${argTypeStr}`,
653
- arg,
654
- DiagnosticCode.ArgumentTypeMismatch
1002
+ this.diagnostics.push(
1003
+ toDiagnostic(Errors.argumentTypeMismatch(i + 1, func.name, paramTypeStr, argTypeStr, arg.range))
655
1004
  );
656
1005
  }
657
1006
  }
@@ -676,12 +1025,13 @@ export class Analyzer {
676
1025
  // Infer a reasonable type for error nodes
677
1026
  node.typeInfo = this.inferErrorNodeType(errorNode, inputType);
678
1027
  // Add diagnostic for the error
679
- this.addDiagnostic(
680
- errorNode.severity || DiagnosticSeverity.Error,
681
- errorNode.message,
682
- errorNode,
683
- errorNode.code?.toString() || DiagnosticCode.ParseError
684
- );
1028
+ this.diagnostics.push({
1029
+ severity: errorNode.severity || DiagnosticSeverity.Error,
1030
+ message: errorNode.message,
1031
+ range: errorNode.range,
1032
+ code: errorNode.code?.toString() || 'FP5003',
1033
+ source: 'fhirpath'
1034
+ });
685
1035
  return;
686
1036
  }
687
1037
 
@@ -696,7 +1046,36 @@ export class Analyzer {
696
1046
 
697
1047
  // For navigation, pass the left's type as input to the right
698
1048
  if (binaryNode.operator === '.') {
699
- this.annotateAST(binaryNode.right, binaryNode.left.typeInfo);
1049
+ // Collect all variables defined in the left side chain
1050
+ const definedVarsWithTypes = this.collectDefinedVariablesWithTypes(binaryNode.left);
1051
+
1052
+ if (definedVarsWithTypes.size > 0) {
1053
+ // Save current variable types
1054
+ const savedTypes = new Map<string, TypeInfo>();
1055
+ definedVarsWithTypes.forEach((type, varName) => {
1056
+ const currentType = this.userVariableTypes.get(varName);
1057
+ if (currentType) {
1058
+ savedTypes.set(varName, currentType);
1059
+ }
1060
+ this.userVariableTypes.set(varName, type);
1061
+ });
1062
+
1063
+ // Annotate right side with new variables in scope
1064
+ this.annotateAST(binaryNode.right, binaryNode.left.typeInfo);
1065
+
1066
+ // Restore previous types
1067
+ definedVarsWithTypes.forEach((_, varName) => {
1068
+ const savedType = savedTypes.get(varName);
1069
+ if (savedType) {
1070
+ this.userVariableTypes.set(varName, savedType);
1071
+ } else {
1072
+ this.userVariableTypes.delete(varName);
1073
+ }
1074
+ });
1075
+ } else {
1076
+ // No defineVariable in chain, proceed normally
1077
+ this.annotateAST(binaryNode.right, binaryNode.left.typeInfo);
1078
+ }
700
1079
  } else {
701
1080
  this.annotateAST(binaryNode.right, inputType);
702
1081
  }
@@ -710,7 +1089,100 @@ export class Analyzer {
710
1089
  case NodeType.Function:
711
1090
  const funcNode = node as FunctionNode;
712
1091
  this.annotateAST(funcNode.name, inputType);
713
- funcNode.arguments.forEach(arg => this.annotateAST(arg, inputType));
1092
+
1093
+ // Special handling for aggregate function arguments
1094
+ if (funcNode.name.type === NodeType.Identifier &&
1095
+ (funcNode.name as IdentifierNode).name === 'aggregate') {
1096
+ // Aggregate establishes both $this and $total
1097
+ if (funcNode.arguments.length >= 1) {
1098
+ const itemType = inputType ? { ...inputType, singleton: true } : { type: 'Any' as TypeName, singleton: true };
1099
+
1100
+ // Save current system variable context
1101
+ const savedThis = this.systemVariableTypes.get('$this');
1102
+ const savedTotal = this.systemVariableTypes.get('$total');
1103
+
1104
+ // Set $this for iteration
1105
+ this.systemVariableTypes.set('$this', itemType);
1106
+
1107
+ if (funcNode.arguments.length >= 2) {
1108
+ // Has init parameter - evaluate it first
1109
+ this.annotateAST(funcNode.arguments[1]!, inputType);
1110
+ const initType = funcNode.arguments[1]!.typeInfo;
1111
+
1112
+ // Set $total to init type
1113
+ if (initType) {
1114
+ this.systemVariableTypes.set('$total', initType);
1115
+ } else {
1116
+ this.systemVariableTypes.set('$total', { type: 'Any', singleton: false });
1117
+ }
1118
+
1119
+ // Process aggregator with both variables set
1120
+ this.annotateAST(funcNode.arguments[0]!, inputType);
1121
+
1122
+ // Process remaining arguments
1123
+ funcNode.arguments.slice(2).forEach(arg => this.annotateAST(arg, inputType));
1124
+ } else {
1125
+ // No init - first pass to infer aggregator type
1126
+ this.systemVariableTypes.set('$total', { type: 'Any', singleton: false });
1127
+ this.annotateAST(funcNode.arguments[0]!, inputType);
1128
+
1129
+ // Second pass with inferred type
1130
+ const aggregatorType = funcNode.arguments[0]!.typeInfo;
1131
+ if (aggregatorType) {
1132
+ this.systemVariableTypes.set('$total', aggregatorType);
1133
+ // Re-annotate with proper $total type
1134
+ this.annotateAST(funcNode.arguments[0]!, inputType);
1135
+ }
1136
+ }
1137
+
1138
+ // Restore previous context
1139
+ if (savedThis) {
1140
+ this.systemVariableTypes.set('$this', savedThis);
1141
+ } else {
1142
+ this.systemVariableTypes.delete('$this');
1143
+ }
1144
+ if (savedTotal) {
1145
+ this.systemVariableTypes.set('$total', savedTotal);
1146
+ } else {
1147
+ this.systemVariableTypes.delete('$total');
1148
+ }
1149
+ }
1150
+ } else {
1151
+ // Special handling for functions that pass their input as context to arguments
1152
+ const funcName = funcNode.name.type === NodeType.Identifier ?
1153
+ (funcNode.name as IdentifierNode).name : null;
1154
+
1155
+ if (funcName && ['where', 'select', 'all', 'exists'].includes(funcName)) {
1156
+ // These functions establish $this as each element of the input collection
1157
+ const elementType = inputType ? { ...inputType, singleton: true } : { type: 'Any' as TypeName, singleton: true };
1158
+
1159
+ // Save current system variable context
1160
+ const savedThis = this.systemVariableTypes.get('$this');
1161
+ const savedIndex = this.systemVariableTypes.get('$index');
1162
+
1163
+ // Set system variables for expression evaluation
1164
+ this.systemVariableTypes.set('$this', elementType);
1165
+ this.systemVariableTypes.set('$index', { type: 'Integer', singleton: true });
1166
+
1167
+ // Process arguments with system variables in scope
1168
+ funcNode.arguments.forEach(arg => this.annotateAST(arg, inputType));
1169
+
1170
+ // Restore previous context
1171
+ if (savedThis) {
1172
+ this.systemVariableTypes.set('$this', savedThis);
1173
+ } else {
1174
+ this.systemVariableTypes.delete('$this');
1175
+ }
1176
+ if (savedIndex) {
1177
+ this.systemVariableTypes.set('$index', savedIndex);
1178
+ } else {
1179
+ this.systemVariableTypes.delete('$index');
1180
+ }
1181
+ } else {
1182
+ // Regular function argument annotation
1183
+ funcNode.arguments.forEach(arg => this.annotateAST(arg, inputType));
1184
+ }
1185
+ }
714
1186
  break;
715
1187
 
716
1188
  case NodeType.Collection: