@atomic-ehr/fhirpath 0.0.1-canary.35b105d.20250724165800
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 +307 -0
- package/dist/index.d.ts +225 -0
- package/dist/index.js +8185 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
- package/src/analyzer/analyzer.ts +486 -0
- package/src/analyzer/model-provider.ts +244 -0
- package/src/analyzer/schemas/index.ts +2 -0
- package/src/analyzer/schemas/types.ts +40 -0
- package/src/analyzer/types.ts +142 -0
- package/src/api/builder.ts +148 -0
- package/src/api/errors.ts +134 -0
- package/src/api/expression.ts +152 -0
- package/src/api/index.ts +57 -0
- package/src/api/registry.ts +128 -0
- package/src/api/types.ts +154 -0
- package/src/compiler/compiler.ts +579 -0
- package/src/compiler/index.ts +2 -0
- package/src/compiler/prototype-context-adapter.ts +99 -0
- package/src/compiler/types.ts +23 -0
- package/src/index.ts +52 -0
- package/src/interpreter/README.md +78 -0
- package/src/interpreter/interpreter.ts +485 -0
- package/src/interpreter/types.ts +110 -0
- package/src/lexer/char-tables.ts +37 -0
- package/src/lexer/errors.ts +31 -0
- package/src/lexer/index.ts +5 -0
- package/src/lexer/lexer.ts +745 -0
- package/src/lexer/token.ts +104 -0
- package/src/parser/ast.ts +123 -0
- package/src/parser/index.ts +3 -0
- package/src/parser/parser.ts +701 -0
- package/src/parser/pprint.ts +169 -0
- package/src/registry/default-analyzers.ts +257 -0
- package/src/registry/default-compilers.ts +31 -0
- package/src/registry/index.ts +93 -0
- package/src/registry/operations/arithmetic.ts +506 -0
- package/src/registry/operations/collection.ts +425 -0
- package/src/registry/operations/comparison.ts +432 -0
- package/src/registry/operations/existence.ts +703 -0
- package/src/registry/operations/filtering.ts +358 -0
- package/src/registry/operations/literals.ts +341 -0
- package/src/registry/operations/logical.ts +402 -0
- package/src/registry/operations/math.ts +128 -0
- package/src/registry/operations/membership.ts +132 -0
- package/src/registry/operations/string.ts +507 -0
- package/src/registry/operations/subsetting.ts +174 -0
- package/src/registry/operations/type-checking.ts +162 -0
- package/src/registry/operations/type-conversion.ts +404 -0
- package/src/registry/operations/type-operators.ts +307 -0
- package/src/registry/operations/utility.ts +542 -0
- package/src/registry/registry.ts +146 -0
- package/src/registry/types.ts +161 -0
- package/src/registry/utils/evaluation-helpers.ts +93 -0
- package/src/registry/utils/index.ts +3 -0
- package/src/registry/utils/type-system.ts +173 -0
- package/src/runtime/context.ts +179 -0
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atomic-ehr/fhirpath",
|
|
3
|
+
"version": "0.0.1-canary.35b105d.20250724165800",
|
|
4
|
+
"description": "A TypeScript implementation of FHIRPath",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup",
|
|
21
|
+
"test": "bun test",
|
|
22
|
+
"typecheck": "bunx tsc --noEmit",
|
|
23
|
+
"prepublishOnly": "bun run build"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/atomic-ehr/fhirpath.git"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"fhir",
|
|
31
|
+
"fhirpath",
|
|
32
|
+
"healthcare",
|
|
33
|
+
"typescript"
|
|
34
|
+
],
|
|
35
|
+
"author": "Atomic EHR Team",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/atomic-ehr/fhirpath/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/atomic-ehr/fhirpath#readme",
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@rgrove/parse-xml": "^4.2.0",
|
|
43
|
+
"@types/bun": "latest",
|
|
44
|
+
"@types/node": "22.13.14",
|
|
45
|
+
"tsup": "8.5.0",
|
|
46
|
+
"typescript": "^5"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"typescript": "^5"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ASTNode,
|
|
3
|
+
LiteralNode,
|
|
4
|
+
IdentifierNode,
|
|
5
|
+
VariableNode,
|
|
6
|
+
BinaryNode,
|
|
7
|
+
UnaryNode,
|
|
8
|
+
FunctionNode,
|
|
9
|
+
CollectionNode,
|
|
10
|
+
IndexNode,
|
|
11
|
+
UnionNode,
|
|
12
|
+
MembershipTestNode,
|
|
13
|
+
TypeCastNode,
|
|
14
|
+
TypeReferenceNode,
|
|
15
|
+
TypeOrIdentifierNode,
|
|
16
|
+
Position
|
|
17
|
+
} from '../parser/ast';
|
|
18
|
+
import { NodeType } from '../parser/ast';
|
|
19
|
+
import { TokenType } from '../lexer/token';
|
|
20
|
+
import type {
|
|
21
|
+
ModelProvider,
|
|
22
|
+
TypeRef,
|
|
23
|
+
TypeAnalysisResult,
|
|
24
|
+
TypeDiagnostic
|
|
25
|
+
} from './types';
|
|
26
|
+
import { AnalysisMode } from './types';
|
|
27
|
+
import { Registry } from '../registry';
|
|
28
|
+
import type { TypeInfo as RegistryTypeInfo, Analyzer as IAnalyzer } from '../registry/types';
|
|
29
|
+
|
|
30
|
+
// Type for node analyzer functions
|
|
31
|
+
type NodeAnalyzer = (node: any, inputType: TypeRef | undefined, inputIsSingleton: boolean) => AnalysisResult;
|
|
32
|
+
|
|
33
|
+
interface AnalysisResult {
|
|
34
|
+
type: TypeRef | undefined;
|
|
35
|
+
isSingleton: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* FHIRPath Type Analyzer - performs type analysis on AST nodes
|
|
40
|
+
* Follows the same pattern as the interpreter but tracks types instead of values
|
|
41
|
+
*/
|
|
42
|
+
export class TypeAnalyzer implements IAnalyzer {
|
|
43
|
+
private diagnostics: TypeDiagnostic[] = [];
|
|
44
|
+
private currentPosition?: Position;
|
|
45
|
+
|
|
46
|
+
// Object lookup for node analyzers (mirrors interpreter pattern)
|
|
47
|
+
private readonly nodeAnalyzers: Record<NodeType, NodeAnalyzer> = {
|
|
48
|
+
[NodeType.Literal]: this.analyzeLiteral.bind(this),
|
|
49
|
+
[NodeType.Identifier]: this.analyzeIdentifier.bind(this),
|
|
50
|
+
[NodeType.TypeOrIdentifier]: this.analyzeTypeOrIdentifier.bind(this),
|
|
51
|
+
[NodeType.Variable]: this.analyzeVariable.bind(this),
|
|
52
|
+
[NodeType.Binary]: this.analyzeBinary.bind(this),
|
|
53
|
+
[NodeType.Unary]: this.analyzeUnary.bind(this),
|
|
54
|
+
[NodeType.Function]: this.analyzeFunction.bind(this),
|
|
55
|
+
[NodeType.Collection]: this.analyzeCollection.bind(this),
|
|
56
|
+
[NodeType.Index]: this.analyzeIndex.bind(this),
|
|
57
|
+
[NodeType.Union]: this.analyzeUnion.bind(this),
|
|
58
|
+
[NodeType.MembershipTest]: this.analyzeMembershipTest.bind(this),
|
|
59
|
+
[NodeType.TypeCast]: this.analyzeTypeCast.bind(this),
|
|
60
|
+
[NodeType.TypeReference]: this.analyzeTypeReference.bind(this),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
private modelProvider: ModelProvider,
|
|
65
|
+
private mode: AnalysisMode = AnalysisMode.Lenient
|
|
66
|
+
) {}
|
|
67
|
+
|
|
68
|
+
// IAnalyzer interface implementation
|
|
69
|
+
error(message: string): void {
|
|
70
|
+
this.addDiagnostic('error', message, this.currentPosition);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
warning(message: string): void {
|
|
74
|
+
this.addDiagnostic('warning', message, this.currentPosition);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
resolveType(typeName: string): TypeRef {
|
|
78
|
+
return this.modelProvider.resolveType(typeName) || this.modelProvider.resolveType('Any')!;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Analyze a FHIRPath expression
|
|
83
|
+
*/
|
|
84
|
+
analyze(
|
|
85
|
+
ast: ASTNode,
|
|
86
|
+
inputType?: TypeRef,
|
|
87
|
+
inputIsSingleton: boolean = true
|
|
88
|
+
): TypeAnalysisResult {
|
|
89
|
+
this.diagnostics = [];
|
|
90
|
+
|
|
91
|
+
const result = this.analyzeNode(ast, inputType, inputIsSingleton);
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
ast,
|
|
95
|
+
diagnostics: this.diagnostics,
|
|
96
|
+
resultType: result.type,
|
|
97
|
+
resultIsSingleton: result.isSingleton
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Main analysis method - uses object lookup
|
|
103
|
+
*/
|
|
104
|
+
private analyzeNode(
|
|
105
|
+
node: ASTNode,
|
|
106
|
+
inputType: TypeRef | undefined,
|
|
107
|
+
inputIsSingleton: boolean
|
|
108
|
+
): AnalysisResult {
|
|
109
|
+
const analyzer = this.nodeAnalyzers[node.type];
|
|
110
|
+
|
|
111
|
+
if (!analyzer) {
|
|
112
|
+
this.addDiagnostic('error', `Unknown node type: ${node.type}`, node.position);
|
|
113
|
+
return { type: this.modelProvider.resolveType('Any'), isSingleton: false };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const result = analyzer(node, inputType, inputIsSingleton);
|
|
117
|
+
|
|
118
|
+
// Annotate the node with type information
|
|
119
|
+
node.resultType = result.type;
|
|
120
|
+
node.isSingleton = result.isSingleton;
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private analyzeLiteral(node: LiteralNode): AnalysisResult {
|
|
126
|
+
// If literal has operation reference from parser
|
|
127
|
+
if (node.operation && node.operation.kind === 'literal') {
|
|
128
|
+
const inputInfo: RegistryTypeInfo = { type: this.resolveType('Any'), isSingleton: true };
|
|
129
|
+
this.currentPosition = node.position;
|
|
130
|
+
const result = node.operation.analyze(this, inputInfo, []);
|
|
131
|
+
return {
|
|
132
|
+
type: result.type,
|
|
133
|
+
isSingleton: result.isSingleton
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Fallback for legacy literals
|
|
138
|
+
let typeName: string;
|
|
139
|
+
|
|
140
|
+
switch (node.valueType) {
|
|
141
|
+
case 'string':
|
|
142
|
+
typeName = 'String';
|
|
143
|
+
break;
|
|
144
|
+
case 'number':
|
|
145
|
+
typeName = Number.isInteger(node.value) ? 'Integer' : 'Decimal';
|
|
146
|
+
break;
|
|
147
|
+
case 'boolean':
|
|
148
|
+
typeName = 'Boolean';
|
|
149
|
+
break;
|
|
150
|
+
case 'date':
|
|
151
|
+
typeName = 'Date';
|
|
152
|
+
break;
|
|
153
|
+
case 'time':
|
|
154
|
+
typeName = 'Time';
|
|
155
|
+
break;
|
|
156
|
+
case 'datetime':
|
|
157
|
+
typeName = 'DateTime';
|
|
158
|
+
break;
|
|
159
|
+
case 'null':
|
|
160
|
+
// null is empty collection
|
|
161
|
+
return { type: undefined, isSingleton: false };
|
|
162
|
+
default:
|
|
163
|
+
typeName = 'Any';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
type: this.modelProvider.resolveType(typeName),
|
|
168
|
+
isSingleton: true
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private analyzeIdentifier(
|
|
173
|
+
node: IdentifierNode,
|
|
174
|
+
inputType: TypeRef | undefined,
|
|
175
|
+
inputIsSingleton: boolean
|
|
176
|
+
): AnalysisResult {
|
|
177
|
+
if (!inputType) {
|
|
178
|
+
// No input type - might be a type name or error
|
|
179
|
+
const typeRef = this.modelProvider.resolveType(node.name);
|
|
180
|
+
if (typeRef) {
|
|
181
|
+
return { type: typeRef, isSingleton: true };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.addDiagnostic('error', `Cannot navigate property '${node.name}' on empty input`, node.position);
|
|
185
|
+
return { type: this.modelProvider.resolveType('Any'), isSingleton: false };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Property navigation
|
|
189
|
+
const propInfo = this.modelProvider.getPropertyType(inputType, node.name);
|
|
190
|
+
|
|
191
|
+
if (!propInfo) {
|
|
192
|
+
this.addDiagnostic(
|
|
193
|
+
this.mode === AnalysisMode.Strict ? 'error' : 'warning',
|
|
194
|
+
`Property '${node.name}' not found on type '${this.modelProvider.getTypeName(inputType)}'`,
|
|
195
|
+
node.position
|
|
196
|
+
);
|
|
197
|
+
return { type: this.modelProvider.resolveType('Any'), isSingleton: false };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// If input is collection, result is always collection (flattening)
|
|
201
|
+
return {
|
|
202
|
+
type: propInfo.type,
|
|
203
|
+
isSingleton: inputIsSingleton && propInfo.isSingleton
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private analyzeTypeOrIdentifier(
|
|
208
|
+
node: TypeOrIdentifierNode,
|
|
209
|
+
inputType: TypeRef | undefined,
|
|
210
|
+
inputIsSingleton: boolean
|
|
211
|
+
): AnalysisResult {
|
|
212
|
+
// First try as type reference
|
|
213
|
+
const typeRef = this.modelProvider.resolveType(node.name);
|
|
214
|
+
if (typeRef && !inputType) {
|
|
215
|
+
return { type: typeRef, isSingleton: true };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Otherwise treat as identifier
|
|
219
|
+
return this.analyzeIdentifier(node as any, inputType, inputIsSingleton);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private analyzeVariable(node: VariableNode): AnalysisResult {
|
|
223
|
+
// For now, assume variables can be Any type
|
|
224
|
+
// In a real implementation, we'd track variable types in context
|
|
225
|
+
return {
|
|
226
|
+
type: this.modelProvider.resolveType('Any'),
|
|
227
|
+
isSingleton: node.name === '$index' // $index is always singleton
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private analyzeBinary(
|
|
232
|
+
node: BinaryNode,
|
|
233
|
+
inputType: TypeRef | undefined,
|
|
234
|
+
inputIsSingleton: boolean
|
|
235
|
+
): AnalysisResult {
|
|
236
|
+
// Special handling for dot operator - it's a pipeline
|
|
237
|
+
if (node.operator === TokenType.DOT) {
|
|
238
|
+
const leftResult = this.analyzeNode(node.left, inputType, inputIsSingleton);
|
|
239
|
+
const rightResult = this.analyzeNode(node.right, leftResult.type, leftResult.isSingleton);
|
|
240
|
+
return rightResult;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Get operation from registry
|
|
244
|
+
const operation = node.operation || Registry.getByToken(node.operator);
|
|
245
|
+
if (!operation || operation.kind !== 'operator') {
|
|
246
|
+
this.addDiagnostic('error', `Unknown operator: ${node.operator}`, node.position);
|
|
247
|
+
return { type: this.modelProvider.resolveType('Any'), isSingleton: true };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Analyze operands
|
|
251
|
+
const leftResult = this.analyzeNode(node.left, inputType, inputIsSingleton);
|
|
252
|
+
const rightResult = this.analyzeNode(node.right, inputType, inputIsSingleton);
|
|
253
|
+
|
|
254
|
+
// Convert to registry TypeInfo format
|
|
255
|
+
const inputInfo: RegistryTypeInfo = { type: inputType || this.resolveType('Any'), isSingleton: inputIsSingleton };
|
|
256
|
+
const leftInfo: RegistryTypeInfo = { type: leftResult.type || this.resolveType('Any'), isSingleton: leftResult.isSingleton };
|
|
257
|
+
const rightInfo: RegistryTypeInfo = { type: rightResult.type || this.resolveType('Any'), isSingleton: rightResult.isSingleton };
|
|
258
|
+
|
|
259
|
+
// Use operation's analyze method
|
|
260
|
+
this.currentPosition = node.position;
|
|
261
|
+
const result = operation.analyze(this, inputInfo, [leftInfo, rightInfo]);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
type: result.type,
|
|
265
|
+
isSingleton: result.isSingleton
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private analyzeUnary(
|
|
270
|
+
node: UnaryNode,
|
|
271
|
+
inputType: TypeRef | undefined,
|
|
272
|
+
inputIsSingleton: boolean
|
|
273
|
+
): AnalysisResult {
|
|
274
|
+
// Get operation from registry
|
|
275
|
+
const operation = node.operation || Registry.getByToken(node.operator);
|
|
276
|
+
if (!operation || operation.kind !== 'operator') {
|
|
277
|
+
this.addDiagnostic('error', `Unknown unary operator: ${node.operator}`, node.position);
|
|
278
|
+
return { type: this.modelProvider.resolveType('Any'), isSingleton: true };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Analyze operand
|
|
282
|
+
const operandResult = this.analyzeNode(node.operand, inputType, inputIsSingleton);
|
|
283
|
+
|
|
284
|
+
// Convert to registry TypeInfo format
|
|
285
|
+
const inputInfo: RegistryTypeInfo = { type: inputType || this.resolveType('Any'), isSingleton: inputIsSingleton };
|
|
286
|
+
const operandInfo: RegistryTypeInfo = { type: operandResult.type || this.resolveType('Any'), isSingleton: operandResult.isSingleton };
|
|
287
|
+
|
|
288
|
+
// Use operation's analyze method
|
|
289
|
+
this.currentPosition = node.position;
|
|
290
|
+
const result = operation.analyze(this, inputInfo, [operandInfo]);
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
type: result.type,
|
|
294
|
+
isSingleton: result.isSingleton
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private analyzeFunction(
|
|
299
|
+
node: FunctionNode,
|
|
300
|
+
inputType: TypeRef | undefined,
|
|
301
|
+
inputIsSingleton: boolean
|
|
302
|
+
): AnalysisResult {
|
|
303
|
+
// Extract function name
|
|
304
|
+
let funcName: string;
|
|
305
|
+
if (node.name.type === NodeType.Identifier) {
|
|
306
|
+
funcName = (node.name as IdentifierNode).name;
|
|
307
|
+
} else {
|
|
308
|
+
this.addDiagnostic('error', 'Complex function names not yet supported', node.position);
|
|
309
|
+
return { type: this.modelProvider.resolveType('Any'), isSingleton: false };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Get function from registry
|
|
313
|
+
const operation = Registry.get(funcName);
|
|
314
|
+
if (!operation || operation.kind !== 'function') {
|
|
315
|
+
this.addDiagnostic('error', `Unknown function: ${funcName}`, node.position);
|
|
316
|
+
return { type: this.modelProvider.resolveType('Any'), isSingleton: false };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Analyze arguments
|
|
320
|
+
const argResults = node.arguments.map(arg => this.analyzeNode(arg, inputType, inputIsSingleton));
|
|
321
|
+
|
|
322
|
+
// Convert to registry TypeInfo format
|
|
323
|
+
const inputInfo: RegistryTypeInfo = { type: inputType || this.resolveType('Any'), isSingleton: inputIsSingleton };
|
|
324
|
+
const argInfos: RegistryTypeInfo[] = argResults.map(r => ({
|
|
325
|
+
type: r.type || this.resolveType('Any'),
|
|
326
|
+
isSingleton: r.isSingleton
|
|
327
|
+
}));
|
|
328
|
+
|
|
329
|
+
// Use operation's analyze method
|
|
330
|
+
this.currentPosition = node.position;
|
|
331
|
+
const result = operation.analyze(this, inputInfo, argInfos);
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
type: result.type,
|
|
335
|
+
isSingleton: result.isSingleton
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private analyzeCollection(
|
|
340
|
+
node: CollectionNode,
|
|
341
|
+
inputType: TypeRef | undefined,
|
|
342
|
+
inputIsSingleton: boolean
|
|
343
|
+
): AnalysisResult {
|
|
344
|
+
if (node.elements.length === 0) {
|
|
345
|
+
// Empty collection
|
|
346
|
+
return { type: undefined, isSingleton: false };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Analyze all elements
|
|
350
|
+
const elementTypes: TypeRef[] = [];
|
|
351
|
+
|
|
352
|
+
for (const element of node.elements) {
|
|
353
|
+
const result = this.analyzeNode(element, inputType, inputIsSingleton);
|
|
354
|
+
if (result.type) {
|
|
355
|
+
elementTypes.push(result.type);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Get common type
|
|
360
|
+
const commonType = this.modelProvider.getCommonType?.(elementTypes) || this.modelProvider.resolveType('Any');
|
|
361
|
+
|
|
362
|
+
return { type: commonType, isSingleton: false };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private analyzeIndex(
|
|
366
|
+
node: IndexNode,
|
|
367
|
+
inputType: TypeRef | undefined,
|
|
368
|
+
inputIsSingleton: boolean
|
|
369
|
+
): AnalysisResult {
|
|
370
|
+
// Analyze the expression being indexed
|
|
371
|
+
const exprResult = this.analyzeNode(node.expression, inputType, inputIsSingleton);
|
|
372
|
+
|
|
373
|
+
// Analyze the index expression
|
|
374
|
+
const indexResult = this.analyzeNode(node.index, exprResult.type, exprResult.isSingleton);
|
|
375
|
+
|
|
376
|
+
// Index must be Integer
|
|
377
|
+
if (indexResult.type) {
|
|
378
|
+
const typeName = this.modelProvider.getTypeName(indexResult.type);
|
|
379
|
+
if (typeName !== 'Integer') {
|
|
380
|
+
this.addDiagnostic('error', 'Index must be an integer', node.position);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!indexResult.isSingleton) {
|
|
385
|
+
this.addDiagnostic('error', 'Index must be singleton', node.position);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Result is singleton of the expression type
|
|
389
|
+
return { type: exprResult.type, isSingleton: true };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private analyzeUnion(
|
|
393
|
+
node: UnionNode,
|
|
394
|
+
inputType: TypeRef | undefined,
|
|
395
|
+
inputIsSingleton: boolean
|
|
396
|
+
): AnalysisResult {
|
|
397
|
+
const types: TypeRef[] = [];
|
|
398
|
+
|
|
399
|
+
for (const operand of node.operands) {
|
|
400
|
+
const result = this.analyzeNode(operand, inputType, inputIsSingleton);
|
|
401
|
+
if (result.type) {
|
|
402
|
+
types.push(result.type);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const commonType = this.modelProvider.getCommonType?.(types) || this.modelProvider.resolveType('Any');
|
|
407
|
+
|
|
408
|
+
return { type: commonType, isSingleton: false };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private analyzeMembershipTest(
|
|
412
|
+
node: MembershipTestNode,
|
|
413
|
+
inputType: TypeRef | undefined,
|
|
414
|
+
inputIsSingleton: boolean
|
|
415
|
+
): AnalysisResult {
|
|
416
|
+
// Analyze the expression
|
|
417
|
+
const exprResult = this.analyzeNode(node.expression, inputType, inputIsSingleton);
|
|
418
|
+
|
|
419
|
+
// Result is Boolean with same cardinality as input
|
|
420
|
+
return {
|
|
421
|
+
type: this.modelProvider.resolveType('Boolean'),
|
|
422
|
+
isSingleton: exprResult.isSingleton
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private analyzeTypeCast(
|
|
427
|
+
node: TypeCastNode,
|
|
428
|
+
inputType: TypeRef | undefined,
|
|
429
|
+
inputIsSingleton: boolean
|
|
430
|
+
): AnalysisResult {
|
|
431
|
+
// Analyze the expression
|
|
432
|
+
const exprResult = this.analyzeNode(node.expression, inputType, inputIsSingleton);
|
|
433
|
+
|
|
434
|
+
// Resolve target type
|
|
435
|
+
const targetType = this.modelProvider.resolveType(node.targetType);
|
|
436
|
+
|
|
437
|
+
if (!targetType) {
|
|
438
|
+
this.addDiagnostic('error', `Unknown type: ${node.targetType}`, node.position);
|
|
439
|
+
return { type: this.modelProvider.resolveType('Any'), isSingleton: exprResult.isSingleton };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Result has target type with same cardinality
|
|
443
|
+
return {
|
|
444
|
+
type: targetType,
|
|
445
|
+
isSingleton: exprResult.isSingleton
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private analyzeTypeReference(node: TypeReferenceNode): AnalysisResult {
|
|
450
|
+
const typeRef = this.modelProvider.resolveType(node.typeName);
|
|
451
|
+
|
|
452
|
+
if (!typeRef) {
|
|
453
|
+
this.addDiagnostic('error', `Unknown type: ${node.typeName}`, node.position);
|
|
454
|
+
return { type: this.modelProvider.resolveType('Any'), isSingleton: true };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return { type: typeRef, isSingleton: true };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private addDiagnostic(
|
|
461
|
+
severity: 'error' | 'warning',
|
|
462
|
+
message: string,
|
|
463
|
+
position?: Position
|
|
464
|
+
) {
|
|
465
|
+
this.diagnostics.push({ severity, message, position });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Helper function to analyze a FHIRPath expression
|
|
471
|
+
*/
|
|
472
|
+
export function analyzeFHIRPath(
|
|
473
|
+
expression: string | ASTNode,
|
|
474
|
+
modelProvider: ModelProvider,
|
|
475
|
+
inputType?: TypeRef,
|
|
476
|
+
mode: AnalysisMode = AnalysisMode.Lenient
|
|
477
|
+
): TypeAnalysisResult {
|
|
478
|
+
// Parse if string
|
|
479
|
+
const ast = typeof expression === 'string'
|
|
480
|
+
? require('../parser').parse(expression)
|
|
481
|
+
: expression;
|
|
482
|
+
|
|
483
|
+
// Create analyzer and analyze
|
|
484
|
+
const analyzer = new TypeAnalyzer(modelProvider, mode);
|
|
485
|
+
return analyzer.analyze(ast, inputType);
|
|
486
|
+
}
|