@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.
- package/README.md +2 -0
- package/dist/index.d.ts +195 -77
- package/dist/index.js +2541 -981
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/analyzer.ts +536 -64
- package/src/boxing.ts +124 -0
- package/src/errors.ts +246 -0
- package/src/index.ts +57 -9
- package/src/inspect.ts +307 -174
- package/src/interpreter.ts +299 -48
- package/src/model-provider.ts +110 -8
- package/src/operations/abs-function.ts +19 -10
- package/src/operations/aggregate-function.ts +11 -3
- package/src/operations/all-function.ts +18 -8
- package/src/operations/allFalse-function.ts +9 -6
- package/src/operations/allTrue-function.ts +9 -6
- package/src/operations/and-operator.ts +20 -11
- package/src/operations/anyFalse-function.ts +6 -4
- package/src/operations/anyTrue-function.ts +6 -4
- package/src/operations/as-operator.ts +1 -0
- package/src/operations/ceiling-function.ts +9 -5
- package/src/operations/children-function.ts +94 -0
- package/src/operations/combine-function.ts +4 -2
- package/src/operations/combine-operator.ts +18 -4
- package/src/operations/contains-function.ts +20 -8
- package/src/operations/contains-operator.ts +14 -6
- package/src/operations/count-function.ts +2 -1
- package/src/operations/defineVariable-function.ts +5 -3
- package/src/operations/descendants-function.ts +62 -0
- package/src/operations/distinct-function.ts +16 -4
- package/src/operations/div-operator.ts +15 -2
- package/src/operations/divide-operator.ts +10 -5
- package/src/operations/dot-operator.ts +3 -1
- package/src/operations/empty-function.ts +2 -1
- package/src/operations/endsWith-function.ts +22 -10
- package/src/operations/equal-operator.ts +18 -6
- package/src/operations/equivalent-operator.ts +15 -10
- package/src/operations/exclude-function.ts +12 -10
- package/src/operations/exists-function.ts +14 -7
- package/src/operations/first-function.ts +7 -4
- package/src/operations/floor-function.ts +9 -5
- package/src/operations/greater-operator.ts +9 -4
- package/src/operations/greater-or-equal-operator.ts +9 -4
- package/src/operations/iif-function.ts +18 -16
- package/src/operations/implies-operator.ts +22 -7
- package/src/operations/in-operator.ts +17 -7
- package/src/operations/index.ts +3 -0
- package/src/operations/indexOf-function.ts +22 -10
- package/src/operations/intersect-function.ts +17 -20
- package/src/operations/is-operator.ts +63 -14
- package/src/operations/isDistinct-function.ts +12 -6
- package/src/operations/join-function.ts +15 -4
- package/src/operations/last-function.ts +7 -4
- package/src/operations/length-function.ts +12 -5
- package/src/operations/less-operator.ts +9 -4
- package/src/operations/less-or-equal-operator.ts +9 -4
- package/src/operations/less-than.ts +25 -1
- package/src/operations/lower-function.ts +12 -5
- package/src/operations/minus-operator.ts +9 -4
- package/src/operations/mod-operator.ts +19 -2
- package/src/operations/multiply-operator.ts +11 -6
- package/src/operations/not-equal-operator.ts +15 -6
- package/src/operations/not-equivalent-operator.ts +15 -10
- package/src/operations/not-function.ts +12 -4
- package/src/operations/ofType-function.ts +135 -0
- package/src/operations/or-operator.ts +20 -11
- package/src/operations/plus-operator.ts +18 -6
- package/src/operations/power-function.ts +21 -9
- package/src/operations/replace-function.ts +26 -9
- package/src/operations/round-function.ts +23 -8
- package/src/operations/select-function.ts +12 -6
- package/src/operations/single-function.ts +5 -3
- package/src/operations/skip-function.ts +12 -5
- package/src/operations/split-function.ts +24 -9
- package/src/operations/sqrt-function.ts +9 -5
- package/src/operations/startsWith-function.ts +20 -8
- package/src/operations/subsetOf-function.ts +14 -11
- package/src/operations/substring-function.ts +36 -19
- package/src/operations/supersetOf-function.ts +14 -11
- package/src/operations/tail-function.ts +3 -1
- package/src/operations/take-function.ts +12 -5
- package/src/operations/toBoolean-function.ts +18 -11
- package/src/operations/toDecimal-function.ts +13 -6
- package/src/operations/toInteger-function.ts +13 -6
- package/src/operations/toString-function.ts +17 -10
- package/src/operations/trace-function.ts +12 -5
- package/src/operations/trim-function.ts +11 -4
- package/src/operations/truncate-function.ts +9 -5
- package/src/operations/unary-minus-operator.ts +22 -12
- package/src/operations/unary-plus-operator.ts +1 -0
- package/src/operations/union-function.ts +19 -24
- package/src/operations/union-operator.ts +1 -0
- package/src/operations/upper-function.ts +12 -5
- package/src/operations/where-function.ts +15 -8
- package/src/operations/xor-operator.ts +8 -3
- package/src/parser.ts +391 -8
- package/src/quantity-value.ts +4 -8
- package/src/registry.ts +3 -3
- package/src/types.ts +10 -6
- 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.
|
|
93
|
+
this.visitMembershipTest(node as MembershipTestNode);
|
|
105
94
|
break;
|
|
106
95
|
case NodeType.TypeCast:
|
|
107
|
-
this.
|
|
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.
|
|
134
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
370
|
+
this.diagnostics.push(
|
|
371
|
+
toDiagnostic(Errors.unknownUserVariable(name, node.range))
|
|
372
|
+
);
|
|
206
373
|
}
|
|
207
374
|
}
|
|
208
375
|
}
|
|
209
|
-
|
|
210
|
-
private
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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.
|
|
345
|
-
|
|
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
|
-
//
|
|
447
|
-
if (node.name
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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.
|
|
630
|
-
|
|
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.
|
|
651
|
-
|
|
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.
|
|
680
|
-
errorNode.severity || DiagnosticSeverity.Error,
|
|
681
|
-
errorNode.message,
|
|
682
|
-
errorNode,
|
|
683
|
-
errorNode.code?.toString() ||
|
|
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
|
-
|
|
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
|
-
|
|
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:
|