@atomic-ehr/fhirpath 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +716 -238
- package/dist/index.d.ts +225 -119
- package/dist/index.js +10911 -5600
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
- package/src/analyzer/augmentor.ts +242 -0
- package/src/analyzer/cursor-services.ts +75 -0
- package/src/analyzer/scope-manager.ts +57 -0
- package/src/analyzer/trivia-indexer.ts +58 -0
- package/src/analyzer/type-compat.ts +157 -0
- package/src/analyzer/utils.ts +132 -0
- package/src/analyzer.ts +921 -1208
- package/src/completion-provider.ts +209 -191
- package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
- package/src/complex-types/temporal.ts +1737 -0
- package/src/errors.ts +25 -3
- package/src/index.ts +17 -104
- package/src/inspect.ts +4 -4
- package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
- package/src/interpreter/navigator.ts +94 -0
- package/src/interpreter/runtime-context.ts +273 -0
- package/src/interpreter.ts +435 -469
- package/src/lexer.ts +188 -210
- package/src/model-provider.ts +71 -43
- package/src/operations/abs-function.ts +1 -1
- package/src/operations/aggregate-function.ts +84 -5
- package/src/operations/all-function.ts +4 -3
- package/src/operations/allFalse-function.ts +2 -1
- package/src/operations/allTrue-function.ts +2 -1
- package/src/operations/and-operator.ts +2 -1
- package/src/operations/anyFalse-function.ts +2 -1
- package/src/operations/anyTrue-function.ts +2 -1
- package/src/operations/as-function.ts +58 -0
- package/src/operations/as-operator.ts +57 -19
- package/src/operations/ceiling-function.ts +1 -1
- package/src/operations/children-function.ts +14 -5
- package/src/operations/combine-function.ts +6 -3
- package/src/operations/combine-operator.ts +6 -7
- package/src/operations/comparison.ts +692 -0
- package/src/operations/contains-function.ts +1 -1
- package/src/operations/contains-operator.ts +2 -1
- package/src/operations/convertsToBoolean-function.ts +78 -0
- package/src/operations/convertsToDecimal-function.ts +82 -0
- package/src/operations/convertsToInteger-function.ts +71 -0
- package/src/operations/convertsToLong-function.ts +89 -0
- package/src/operations/convertsToQuantity-function.ts +116 -0
- package/src/operations/convertsToString-function.ts +88 -0
- package/src/operations/count-function.ts +2 -1
- package/src/operations/dateOf-function.ts +69 -0
- package/src/operations/dayOf-function.ts +66 -0
- package/src/operations/decimal-boundaries.ts +133 -0
- package/src/operations/defineVariable-function.ts +130 -17
- package/src/operations/distinct-function.ts +1 -1
- package/src/operations/div-operator.ts +1 -1
- package/src/operations/divide-operator.ts +12 -7
- package/src/operations/dot-operator.ts +1 -1
- package/src/operations/empty-function.ts +30 -21
- package/src/operations/endsWith-function.ts +6 -1
- package/src/operations/equal-operator.ts +23 -32
- package/src/operations/equivalent-operator.ts +13 -53
- package/src/operations/exclude-function.ts +2 -1
- package/src/operations/exists-function.ts +4 -3
- package/src/operations/first-function.ts +1 -1
- package/src/operations/floor-function.ts +1 -1
- package/src/operations/greater-operator.ts +20 -3
- package/src/operations/greater-or-equal-operator.ts +20 -3
- package/src/operations/highBoundary-function.ts +120 -0
- package/src/operations/hourOf-function.ts +66 -0
- package/src/operations/iif-function.ts +186 -7
- package/src/operations/implies-operator.ts +1 -1
- package/src/operations/in-operator.ts +2 -1
- package/src/operations/index.ts +41 -0
- package/src/operations/indexOf-function.ts +1 -1
- package/src/operations/intersect-function.ts +1 -1
- package/src/operations/is-function.ts +59 -0
- package/src/operations/is-operator.ts +20 -9
- package/src/operations/isDistinct-function.ts +2 -1
- package/src/operations/join-function.ts +1 -1
- package/src/operations/last-function.ts +1 -1
- package/src/operations/lastIndexOf-function.ts +85 -0
- package/src/operations/length-function.ts +1 -1
- package/src/operations/less-operator.ts +20 -3
- package/src/operations/less-or-equal-operator.ts +20 -3
- package/src/operations/less-than.ts +2 -2
- package/src/operations/lowBoundary-function.ts +120 -0
- package/src/operations/lower-function.ts +1 -1
- package/src/operations/matches-function.ts +86 -0
- package/src/operations/matchesFull-function.ts +96 -0
- package/src/operations/millisecondOf-function.ts +66 -0
- package/src/operations/minus-operator.ts +69 -4
- package/src/operations/minuteOf-function.ts +66 -0
- package/src/operations/mod-operator.ts +1 -1
- package/src/operations/monthOf-function.ts +66 -0
- package/src/operations/multiply-operator.ts +27 -3
- package/src/operations/not-equal-operator.ts +24 -30
- package/src/operations/not-equivalent-operator.ts +13 -53
- package/src/operations/not-function.ts +1 -1
- package/src/operations/ofType-function.ts +8 -12
- package/src/operations/or-operator.ts +2 -1
- package/src/operations/plus-operator.ts +71 -7
- package/src/operations/power-function.ts +35 -10
- package/src/operations/repeat-function.ts +169 -0
- package/src/operations/replace-function.ts +1 -1
- package/src/operations/replaceMatches-function.ts +120 -0
- package/src/operations/round-function.ts +1 -1
- package/src/operations/secondOf-function.ts +66 -0
- package/src/operations/select-function.ts +66 -5
- package/src/operations/single-function.ts +1 -1
- package/src/operations/skip-function.ts +1 -1
- package/src/operations/split-function.ts +1 -1
- package/src/operations/sqrt-function.ts +15 -8
- package/src/operations/startsWith-function.ts +1 -1
- package/src/operations/subsetOf-function.ts +6 -2
- package/src/operations/substring-function.ts +1 -1
- package/src/operations/supersetOf-function.ts +6 -2
- package/src/operations/tail-function.ts +1 -1
- package/src/operations/take-function.ts +1 -1
- package/src/operations/temporal-functions.ts +555 -0
- package/src/operations/timeOf-function.ts +67 -0
- package/src/operations/timezoneOffsetOf-function.ts +69 -0
- package/src/operations/toBoolean-function.ts +27 -8
- package/src/operations/toChars-function.ts +56 -0
- package/src/operations/toDecimal-function.ts +27 -8
- package/src/operations/toInteger-function.ts +15 -3
- package/src/operations/toLong-function.ts +98 -0
- package/src/operations/toQuantity-function.ts +181 -0
- package/src/operations/toString-function.ts +45 -3
- package/src/operations/trace-function.ts +1 -1
- package/src/operations/trim-function.ts +1 -1
- package/src/operations/truncate-function.ts +1 -1
- package/src/operations/unary-minus-operator.ts +2 -2
- package/src/operations/unary-plus-operator.ts +1 -1
- package/src/operations/union-function.ts +1 -1
- package/src/operations/union-operator.ts +16 -26
- package/src/operations/upper-function.ts +1 -1
- package/src/operations/where-function.ts +3 -3
- package/src/operations/xor-operator.ts +1 -1
- package/src/operations/yearOf-function.ts +66 -0
- package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
- package/src/parser.ts +248 -501
- package/src/registry.ts +53 -42
- package/src/types.ts +128 -16
- package/src/utils/pprint.ts +151 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atomic-ehr/fhirpath",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "A TypeScript implementation of FHIRPath",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsup",
|
|
21
|
-
"test": "
|
|
21
|
+
"test": "./scripts/test.sh",
|
|
22
|
+
"test:output": "bun test test/*",
|
|
22
23
|
"typecheck": "bunx tsc --noEmit",
|
|
23
24
|
"prepublishOnly": "bun run build"
|
|
24
25
|
},
|
|
@@ -42,12 +43,16 @@
|
|
|
42
43
|
"@anthropic-ai/sdk": "^0.57.0",
|
|
43
44
|
"@rgrove/parse-xml": "^4.2.0",
|
|
44
45
|
"@types/bun": "latest",
|
|
46
|
+
"@types/js-yaml": "^4.0.9",
|
|
45
47
|
"@types/jsdom": "^21.1.7",
|
|
48
|
+
"@types/lodash": "^4.17.20",
|
|
46
49
|
"@types/node": "22.13.14",
|
|
47
50
|
"@types/turndown": "^5.0.5",
|
|
48
51
|
"antlr4ts": "^0.5.0-alpha.4",
|
|
49
52
|
"antlr4ts-cli": "^0.5.0-alpha.4",
|
|
53
|
+
"js-yaml": "^4.1.0",
|
|
50
54
|
"jsdom": "^26.1.0",
|
|
55
|
+
"lodash": "^4.17.21",
|
|
51
56
|
"tsup": "8.5.0",
|
|
52
57
|
"turndown": "^7.2.0",
|
|
53
58
|
"typescript": "^5.8.3",
|
|
@@ -57,8 +62,8 @@
|
|
|
57
62
|
"typescript": "^5"
|
|
58
63
|
},
|
|
59
64
|
"dependencies": {
|
|
60
|
-
"@atomic-ehr/fhir-canonical-manager": "
|
|
61
|
-
"@atomic-ehr/fhirschema": "
|
|
65
|
+
"@atomic-ehr/fhir-canonical-manager": "^0.0.11",
|
|
66
|
+
"@atomic-ehr/fhirschema": "^0.0.2",
|
|
62
67
|
"@atomic-ehr/ucum": "^0.2.5"
|
|
63
68
|
}
|
|
64
69
|
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { NodeType } from '../types';
|
|
2
|
+
import type { ASTNode, BinaryNode, UnaryNode, FunctionNode, IndexNode, CollectionNode, MembershipTestNode, TypeCastNode, TypeReferenceNode, QuantityNode } from '../types';
|
|
3
|
+
import type { TriviaInfo } from '../types';
|
|
4
|
+
import type { AnyCursorNode } from '../parser/cursor-nodes';
|
|
5
|
+
import { isCursorNode, createCursorTypeNode } from '../parser/cursor-nodes';
|
|
6
|
+
|
|
7
|
+
export interface AugmentationIndexes {
|
|
8
|
+
nodeById: Map<string, ASTNode>;
|
|
9
|
+
nodesByType: Map<NodeType | 'Error' | 'CursorNode', ASTNode[]>;
|
|
10
|
+
identifiers: Map<string, ASTNode[]>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface AugmentationOptions {
|
|
14
|
+
input: string;
|
|
15
|
+
preserveTrivia?: boolean;
|
|
16
|
+
trivia?: {
|
|
17
|
+
leadingByStart?: Map<number, TriviaInfo[]>;
|
|
18
|
+
trailingByEnd?: Map<number, TriviaInfo[]>;
|
|
19
|
+
};
|
|
20
|
+
cursorPosition?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AugmentationResult {
|
|
24
|
+
ast: ASTNode;
|
|
25
|
+
indexes: AugmentationIndexes;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function augment(ast: ASTNode, opts: AugmentationOptions): AugmentationResult {
|
|
29
|
+
const indexes: AugmentationIndexes = {
|
|
30
|
+
nodeById: new Map(),
|
|
31
|
+
nodesByType: new Map(),
|
|
32
|
+
identifiers: new Map(),
|
|
33
|
+
};
|
|
34
|
+
let nodeIdCounter = 0;
|
|
35
|
+
|
|
36
|
+
function enrich(node: ASTNode, parent?: ASTNode): void {
|
|
37
|
+
(node as any).id = `node_${nodeIdCounter++}`;
|
|
38
|
+
const start = node.range.start.offset ?? 0;
|
|
39
|
+
const end = node.range.end.offset ?? start;
|
|
40
|
+
(node as any).raw = opts.input.substring(start, end);
|
|
41
|
+
if (opts.preserveTrivia && opts.trivia) {
|
|
42
|
+
const s = node.range.start.offset ?? -1;
|
|
43
|
+
const e = node.range.end.offset ?? -1;
|
|
44
|
+
(node as any).leadingTrivia = opts.trivia.leadingByStart?.get(s) ?? [];
|
|
45
|
+
(node as any).trailingTrivia = opts.trivia.trailingByEnd?.get(e) ?? [];
|
|
46
|
+
}
|
|
47
|
+
if (parent) {
|
|
48
|
+
(node as any).parent = parent;
|
|
49
|
+
}
|
|
50
|
+
indexes.nodeById.set((node as any).id, node);
|
|
51
|
+
const bucket = indexes.nodesByType.get(node.type) || [];
|
|
52
|
+
bucket.push(node);
|
|
53
|
+
indexes.nodesByType.set(node.type, bucket);
|
|
54
|
+
if ((node as any).type === NodeType.Identifier) {
|
|
55
|
+
const name = (node as any).name as string;
|
|
56
|
+
const arr = indexes.identifiers.get(name) || [];
|
|
57
|
+
arr.push(node);
|
|
58
|
+
indexes.identifiers.set(name, arr);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function visit(node: ASTNode, parent?: ASTNode): void {
|
|
63
|
+
enrich(node, parent);
|
|
64
|
+
switch (node.type) {
|
|
65
|
+
case NodeType.Binary: {
|
|
66
|
+
const bin = node as BinaryNode;
|
|
67
|
+
(node as any).children = [bin.left, bin.right];
|
|
68
|
+
visit(bin.left, node);
|
|
69
|
+
visit(bin.right, node);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case NodeType.Unary: {
|
|
73
|
+
const un = node as UnaryNode;
|
|
74
|
+
(node as any).children = [un.operand];
|
|
75
|
+
visit(un.operand, node);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case NodeType.Function: {
|
|
79
|
+
const fn = node as FunctionNode;
|
|
80
|
+
(node as any).children = [fn.name, ...fn.arguments];
|
|
81
|
+
visit(fn.name, node);
|
|
82
|
+
for (const arg of fn.arguments) visit(arg, node);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case NodeType.Index: {
|
|
86
|
+
const idx = node as IndexNode;
|
|
87
|
+
(node as any).children = [idx.expression, idx.index];
|
|
88
|
+
visit(idx.expression, node);
|
|
89
|
+
visit(idx.index, node);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
case NodeType.Collection: {
|
|
93
|
+
const coll = node as CollectionNode;
|
|
94
|
+
(node as any).children = [...coll.elements];
|
|
95
|
+
for (const el of coll.elements) visit(el, node);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case NodeType.MembershipTest: {
|
|
99
|
+
const mt = node as MembershipTestNode;
|
|
100
|
+
(node as any).children = [mt.expression];
|
|
101
|
+
visit(mt.expression, node);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
case NodeType.TypeCast: {
|
|
105
|
+
const tc = node as TypeCastNode;
|
|
106
|
+
(node as any).children = [tc.expression];
|
|
107
|
+
visit(tc.expression, node);
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
default:
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// First pass: enrich and index
|
|
116
|
+
visit(ast);
|
|
117
|
+
|
|
118
|
+
// Cursor-specific transform for ofType arguments
|
|
119
|
+
if (opts.cursorPosition !== undefined) {
|
|
120
|
+
const cursorPos = opts.cursorPosition;
|
|
121
|
+
function transform(node: ASTNode): ASTNode {
|
|
122
|
+
switch (node.type) {
|
|
123
|
+
case NodeType.Binary: {
|
|
124
|
+
const binary = node as BinaryNode;
|
|
125
|
+
if (binary.right.type === NodeType.Function) {
|
|
126
|
+
const func = binary.right as FunctionNode;
|
|
127
|
+
if ((func.name as any).name === 'ofType') {
|
|
128
|
+
func.arguments = func.arguments.map((arg) => {
|
|
129
|
+
if (isCursorNode(arg)) {
|
|
130
|
+
const cursorNode = arg as AnyCursorNode;
|
|
131
|
+
return createCursorTypeNode(cursorNode.position, 'ofType') as any;
|
|
132
|
+
}
|
|
133
|
+
if (arg.type === NodeType.Binary) {
|
|
134
|
+
const binaryArg = arg as BinaryNode;
|
|
135
|
+
if (isCursorNode(binaryArg.right)) {
|
|
136
|
+
const cursorNode = binaryArg.right as AnyCursorNode;
|
|
137
|
+
let partialText: string | undefined;
|
|
138
|
+
if (binaryArg.left.type === NodeType.Identifier) {
|
|
139
|
+
partialText = (binaryArg.left as any).name;
|
|
140
|
+
}
|
|
141
|
+
return createCursorTypeNode(cursorNode.position, 'ofType', partialText) as any;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// New: identifier immediately before cursor inside ofType
|
|
145
|
+
if ((arg.type === NodeType.Identifier) && (arg as any).range?.end?.offset === cursorPos) {
|
|
146
|
+
const name = (arg as any).name as string | undefined;
|
|
147
|
+
return createCursorTypeNode(cursorPos, 'ofType', name) as any;
|
|
148
|
+
}
|
|
149
|
+
return arg;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
binary.left = transform(binary.left);
|
|
153
|
+
} else {
|
|
154
|
+
binary.left = transform(binary.left);
|
|
155
|
+
binary.right = transform(binary.right);
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case NodeType.Function: {
|
|
160
|
+
const func = node as FunctionNode;
|
|
161
|
+
if ((func.name as any).name === 'ofType') {
|
|
162
|
+
func.arguments = func.arguments.map((arg) => {
|
|
163
|
+
if (isCursorNode(arg)) {
|
|
164
|
+
const cursorNode = arg as AnyCursorNode;
|
|
165
|
+
return createCursorTypeNode(cursorNode.position, 'ofType') as any;
|
|
166
|
+
}
|
|
167
|
+
if (arg.type === NodeType.Binary) {
|
|
168
|
+
const binaryArg = arg as BinaryNode;
|
|
169
|
+
if (isCursorNode(binaryArg.right)) {
|
|
170
|
+
const cursorNode = binaryArg.right as AnyCursorNode;
|
|
171
|
+
let partialText: string | undefined;
|
|
172
|
+
if (binaryArg.left.type === NodeType.Identifier) {
|
|
173
|
+
partialText = (binaryArg.left as any).name;
|
|
174
|
+
}
|
|
175
|
+
return createCursorTypeNode(cursorNode.position, 'ofType', partialText) as any;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// New: identifier immediately before cursor inside ofType
|
|
179
|
+
if ((arg.type === NodeType.Identifier) && (arg as any).range?.end?.offset === cursorPos) {
|
|
180
|
+
const name = (arg as any).name as string | undefined;
|
|
181
|
+
return createCursorTypeNode(cursorPos, 'ofType', name) as any;
|
|
182
|
+
}
|
|
183
|
+
return arg;
|
|
184
|
+
});
|
|
185
|
+
// If we still have a cursor type node without partial text, try to infer from source between '(' and cursor
|
|
186
|
+
if (func.arguments.length >= 1) {
|
|
187
|
+
const firstArg = func.arguments[0] as any;
|
|
188
|
+
if (isCursorNode(firstArg) && (firstArg as any).context === 'type' && (firstArg as any).partialText == null) {
|
|
189
|
+
const nameEnd = (func.name as any).range?.end?.offset ?? -1;
|
|
190
|
+
const start = nameEnd + 1; // after '('
|
|
191
|
+
const end = (firstArg as any).position ?? start;
|
|
192
|
+
if (start >= 0 && end >= start) {
|
|
193
|
+
const slice = opts.input.slice(start, end).trim();
|
|
194
|
+
const m = slice.match(/[A-Za-z][A-Za-z0-9.]*$/);
|
|
195
|
+
const inferred = m ? m[0] : undefined;
|
|
196
|
+
if (inferred && inferred.length > 0) {
|
|
197
|
+
func.arguments[0] = createCursorTypeNode(end, 'ofType', inferred) as any;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
func.arguments = func.arguments.map(arg => transform(arg));
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case NodeType.Unary: {
|
|
208
|
+
const unary = node as UnaryNode;
|
|
209
|
+
unary.operand = transform(unary.operand);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
case NodeType.Index: {
|
|
213
|
+
const idx = node as IndexNode;
|
|
214
|
+
idx.expression = transform(idx.expression);
|
|
215
|
+
idx.index = transform(idx.index);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
case NodeType.Collection: {
|
|
219
|
+
const coll = node as CollectionNode;
|
|
220
|
+
coll.elements = coll.elements.map(el => transform(el));
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
case NodeType.MembershipTest: {
|
|
224
|
+
const mt = node as MembershipTestNode;
|
|
225
|
+
mt.expression = transform(mt.expression);
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case NodeType.TypeCast: {
|
|
229
|
+
const tc = node as TypeCastNode;
|
|
230
|
+
tc.expression = transform(tc.expression);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return node;
|
|
235
|
+
}
|
|
236
|
+
transform(ast);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { ast, indexes };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// (legacy transform function removed; handled inline above)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { TokenType } from '../lexer';
|
|
2
|
+
import { NodeType } from '../types';
|
|
3
|
+
import type { ASTNode, IdentifierNode, BinaryNode, UnaryNode, FunctionNode, IndexNode, MembershipTestNode, TypeCastNode } from '../types';
|
|
4
|
+
|
|
5
|
+
export function findNodeAtPosition(root: ASTNode, offset: number): ASTNode | null {
|
|
6
|
+
if (offset < root.range.start.offset! || offset > root.range.end.offset!) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
if ('children' in root && Array.isArray((root as any).children)) {
|
|
10
|
+
for (const child of (root as any).children as ASTNode[]) {
|
|
11
|
+
const found = findNodeAtPosition(child, offset);
|
|
12
|
+
if (found) return found;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
switch (root.type) {
|
|
16
|
+
case NodeType.Binary: {
|
|
17
|
+
const bin = root as BinaryNode;
|
|
18
|
+
return findNodeAtPosition(bin.left, offset) || findNodeAtPosition(bin.right, offset) || root;
|
|
19
|
+
}
|
|
20
|
+
case NodeType.Unary: {
|
|
21
|
+
const un = root as UnaryNode;
|
|
22
|
+
return findNodeAtPosition(un.operand, offset) || root;
|
|
23
|
+
}
|
|
24
|
+
case NodeType.Function: {
|
|
25
|
+
const fn = root as FunctionNode;
|
|
26
|
+
const nameRes = findNodeAtPosition(fn.name, offset);
|
|
27
|
+
if (nameRes) return nameRes;
|
|
28
|
+
for (const arg of fn.arguments) {
|
|
29
|
+
const argRes = findNodeAtPosition(arg, offset);
|
|
30
|
+
if (argRes) return argRes;
|
|
31
|
+
}
|
|
32
|
+
return root;
|
|
33
|
+
}
|
|
34
|
+
default:
|
|
35
|
+
return root;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getExpectedTokens(node: ASTNode | null): TokenType[] {
|
|
40
|
+
if (!node) return getExpectedTokensForError();
|
|
41
|
+
switch (node.type) {
|
|
42
|
+
case NodeType.Binary:
|
|
43
|
+
return [TokenType.DOT, TokenType.LBRACKET];
|
|
44
|
+
case NodeType.Identifier:
|
|
45
|
+
return [TokenType.DOT, TokenType.LPAREN, TokenType.LBRACKET];
|
|
46
|
+
default:
|
|
47
|
+
return getExpectedTokensForError();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function getExpectedTokensForError(): TokenType[] {
|
|
52
|
+
return [
|
|
53
|
+
TokenType.EOF,
|
|
54
|
+
TokenType.DOT,
|
|
55
|
+
TokenType.LBRACKET,
|
|
56
|
+
TokenType.LPAREN,
|
|
57
|
+
TokenType.OPERATOR,
|
|
58
|
+
TokenType.IDENTIFIER,
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getCompletions(node: ASTNode | null, identifiers?: Map<string, ASTNode[]>): string[] {
|
|
63
|
+
if (!node) return [];
|
|
64
|
+
const completions: string[] = [];
|
|
65
|
+
if (identifiers) {
|
|
66
|
+
for (const name of Array.from(identifiers.keys())) {
|
|
67
|
+
completions.push(name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
completions.push(
|
|
71
|
+
'where', 'select', 'first', 'last', 'tail',
|
|
72
|
+
'skip', 'take', 'count', 'empty', 'exists'
|
|
73
|
+
);
|
|
74
|
+
return completions;
|
|
75
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { TypeInfo } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Manages lexical scopes for variables within the FHIRPath Analyzer.
|
|
5
|
+
* Provides a stack-based mechanism to enter and leave scopes,
|
|
6
|
+
* ensuring correct variable resolution based on their definition context.
|
|
7
|
+
*/
|
|
8
|
+
export class ScopeManager<T = TypeInfo> {
|
|
9
|
+
// Each element in the array represents a scope. The last element is the current scope.
|
|
10
|
+
// Each scope is a Map from variable name (e.g., '$this', '$index') to its TypeInfo.
|
|
11
|
+
private scopes: Map<string, T>[] = [new Map()];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Enters a new, empty scope.
|
|
15
|
+
* Variables defined after entering this scope will be local to it.
|
|
16
|
+
*/
|
|
17
|
+
enterScope(): void {
|
|
18
|
+
this.scopes.push(new Map());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Leaves the current scope.
|
|
23
|
+
* If there's only one scope left (the global scope), it cannot be left.
|
|
24
|
+
*/
|
|
25
|
+
leaveScope(): void {
|
|
26
|
+
if (this.scopes.length > 1) {
|
|
27
|
+
this.scopes.pop();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sets a variable's type in the current scope.
|
|
33
|
+
* @param name The name of the variable (e.g., '$this').
|
|
34
|
+
* @param type The TypeInfo of the variable.
|
|
35
|
+
*/
|
|
36
|
+
set(name: string, type: T): void {
|
|
37
|
+
if (this.scopes.length > 0) {
|
|
38
|
+
(this.scopes[this.scopes.length - 1] as Map<string, T>).set(name, type);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Retrieves a variable's type, searching from the current scope upwards through parent scopes.
|
|
44
|
+
* @param name The name of the variable.
|
|
45
|
+
* @returns The TypeInfo of the variable, or undefined if not found in any active scope.
|
|
46
|
+
*/
|
|
47
|
+
get(name: string): T | undefined {
|
|
48
|
+
// Search from the innermost (current) scope outwards to the global scope
|
|
49
|
+
for (let i = this.scopes.length - 1; i >= 0; i--) {
|
|
50
|
+
const currentScope = this.scopes[i];
|
|
51
|
+
if (currentScope && currentScope.has(name)) {
|
|
52
|
+
return currentScope.get(name) as T;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Channel, TokenType } from '../lexer';
|
|
2
|
+
import type { Token } from '../lexer';
|
|
3
|
+
import type { TriviaInfo } from '../types';
|
|
4
|
+
|
|
5
|
+
export interface TriviaSpans {
|
|
6
|
+
leadingByStart: Map<number, TriviaInfo[]>;
|
|
7
|
+
trailingByEnd: Map<number, TriviaInfo[]>;
|
|
8
|
+
tokenByStart: Map<number, Token>;
|
|
9
|
+
tokenByEnd: Map<number, Token>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function computeTriviaSpans(allTokens: Token[]): TriviaSpans {
|
|
13
|
+
const leadingByStart = new Map<number, TriviaInfo[]>();
|
|
14
|
+
const trailingByEnd = new Map<number, TriviaInfo[]>();
|
|
15
|
+
const tokenByStart = new Map<number, Token>();
|
|
16
|
+
const tokenByEnd = new Map<number, Token>();
|
|
17
|
+
|
|
18
|
+
let acc: TriviaInfo[] = [];
|
|
19
|
+
let lastDefault: Token | undefined;
|
|
20
|
+
|
|
21
|
+
const toTrivia = (token: Token): TriviaInfo | null => {
|
|
22
|
+
const range = token.range || {
|
|
23
|
+
start: { line: 0, character: 0, offset: token.start },
|
|
24
|
+
end: { line: 0, character: 0, offset: token.end }
|
|
25
|
+
};
|
|
26
|
+
if (token.type === TokenType.WHITESPACE) {
|
|
27
|
+
return { type: 'whitespace', value: token.value, range };
|
|
28
|
+
}
|
|
29
|
+
if (token.type === TokenType.LINE_COMMENT) {
|
|
30
|
+
return { type: 'lineComment', value: token.value, range };
|
|
31
|
+
}
|
|
32
|
+
if (token.type === TokenType.BLOCK_COMMENT) {
|
|
33
|
+
return { type: 'comment', value: token.value, range };
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
for (const tok of allTokens) {
|
|
39
|
+
const isHidden = tok.channel === Channel.HIDDEN;
|
|
40
|
+
if (isHidden) {
|
|
41
|
+
const trivia = toTrivia(tok);
|
|
42
|
+
if (trivia) acc.push(trivia);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const accCopy = acc.length ? acc.slice() : [];
|
|
46
|
+
if (lastDefault) {
|
|
47
|
+
trailingByEnd.set(lastDefault.end, accCopy);
|
|
48
|
+
}
|
|
49
|
+
leadingByStart.set(tok.start, accCopy);
|
|
50
|
+
tokenByStart.set(tok.start, tok);
|
|
51
|
+
tokenByEnd.set(tok.end, tok);
|
|
52
|
+
acc = [];
|
|
53
|
+
lastDefault = tok;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { leadingByStart, trailingByEnd, tokenByStart, tokenByEnd };
|
|
57
|
+
}
|
|
58
|
+
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TypeInfo,
|
|
3
|
+
OperatorDefinition,
|
|
4
|
+
OperatorSignature,
|
|
5
|
+
FunctionDefinition,
|
|
6
|
+
FunctionSignature,
|
|
7
|
+
TypeName,
|
|
8
|
+
} from '../types';
|
|
9
|
+
import { registry } from '../registry';
|
|
10
|
+
|
|
11
|
+
export function isTypeCompatible(source: TypeInfo, target: TypeInfo): boolean {
|
|
12
|
+
return registry.isTypeCompatible(source, target);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function matchOperatorSignature(
|
|
16
|
+
left: TypeInfo,
|
|
17
|
+
right: TypeInfo,
|
|
18
|
+
def?: OperatorDefinition
|
|
19
|
+
): OperatorSignature | undefined {
|
|
20
|
+
if (!def || !def.signatures || def.signatures.length === 0) return undefined;
|
|
21
|
+
|
|
22
|
+
let best: { sig: OperatorSignature; score: number; tieBias: number } | undefined;
|
|
23
|
+
|
|
24
|
+
for (const sig of def.signatures) {
|
|
25
|
+
const leftOk = isTypeCompatible(left, sig.left);
|
|
26
|
+
const rightOk = isTypeCompatible(right, sig.right);
|
|
27
|
+
if (!leftOk || !rightOk) continue;
|
|
28
|
+
|
|
29
|
+
const leftScore = specificity(left, sig.left);
|
|
30
|
+
const rightScore = specificity(right, sig.right);
|
|
31
|
+
let score = leftScore + rightScore;
|
|
32
|
+
|
|
33
|
+
// Prefer decimal math when any operand is Decimal
|
|
34
|
+
const anyActualDecimal = left.type === 'Decimal' || right.type === 'Decimal';
|
|
35
|
+
const sigHasDecimal = sig.left.type === 'Decimal' || sig.right.type === 'Decimal';
|
|
36
|
+
const tieBias = anyActualDecimal && sigHasDecimal ? 1 : 0;
|
|
37
|
+
|
|
38
|
+
if (!best || score > best.score || (score === best.score && tieBias > best.tieBias)) {
|
|
39
|
+
best = { sig, score, tieBias };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return best?.sig;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function specificity(actual: TypeInfo, required: TypeInfo): number {
|
|
47
|
+
if (required.type === 'Any') return 0;
|
|
48
|
+
if (actual.type === required.type) return 3;
|
|
49
|
+
// numeric widening
|
|
50
|
+
const numeric = new Set<TypeName>(['Integer', 'Decimal']);
|
|
51
|
+
if (numeric.has(actual.type) && numeric.has(required.type)) return 2;
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function matchFunctionSignature(
|
|
56
|
+
input: TypeInfo,
|
|
57
|
+
args: TypeInfo[],
|
|
58
|
+
def?: FunctionDefinition
|
|
59
|
+
): FunctionSignature | undefined {
|
|
60
|
+
if (!def || !def.signatures || def.signatures.length === 0) return undefined;
|
|
61
|
+
|
|
62
|
+
let best: { sig: FunctionSignature; score: number; tieBias: number } | undefined;
|
|
63
|
+
|
|
64
|
+
for (const sig of def.signatures) {
|
|
65
|
+
// Input compatibility (if specified)
|
|
66
|
+
if (sig.input && !isFunctionTypeCompatible(input, sig.input)) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let ok = true;
|
|
71
|
+
let score = 0;
|
|
72
|
+
let tieBias = 0;
|
|
73
|
+
const params = sig.parameters || [];
|
|
74
|
+
for (let i = 0; i < Math.min(args.length, params.length); i++) {
|
|
75
|
+
const argType = args[i]!;
|
|
76
|
+
const param = params[i]!;
|
|
77
|
+
if (param.expression) {
|
|
78
|
+
continue; // expression params are analyzed in their own context
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const isEmptyArg = argType.isEmpty || (argType.type === 'Any' && !argType.singleton);
|
|
82
|
+
if (isEmptyArg && !def.doesNotPropagateEmpty) {
|
|
83
|
+
continue; // empty propagates; don't penalize
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!isFunctionTypeCompatible(argType, param.type)) {
|
|
87
|
+
ok = false;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
score += specificity(argType, param.type);
|
|
91
|
+
|
|
92
|
+
// Prefer decimal params when actual is Decimal
|
|
93
|
+
if (argType.type === 'Decimal' && param.type.type === 'Decimal') {
|
|
94
|
+
tieBias += 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!ok) continue;
|
|
99
|
+
|
|
100
|
+
// Add input specificity
|
|
101
|
+
if (sig.input) {
|
|
102
|
+
score += specificity(input, sig.input);
|
|
103
|
+
if (input.type === 'Decimal' && sig.input.type === 'Decimal') tieBias += 1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!best || score > best.score || (score === best.score && tieBias > best.tieBias)) {
|
|
107
|
+
best = { sig, score, tieBias };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return best?.sig;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isFunctionTypeCompatible(actual: TypeInfo, expected: TypeInfo): boolean {
|
|
115
|
+
// Enforce singleton when required
|
|
116
|
+
if (expected.singleton && !actual.singleton) return false;
|
|
117
|
+
if (expected.type === 'Any') return true;
|
|
118
|
+
if (actual.type === expected.type) return true;
|
|
119
|
+
// Allow Integer to be used where Decimal is expected (promotion)
|
|
120
|
+
if (expected.type === 'Decimal' && actual.type === 'Integer') return true;
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export type ResultSpec =
|
|
125
|
+
| TypeInfo
|
|
126
|
+
| 'inputType'
|
|
127
|
+
| 'inputTypeSingleton'
|
|
128
|
+
| 'leftType'
|
|
129
|
+
| 'rightType'
|
|
130
|
+
| 'parameterType';
|
|
131
|
+
|
|
132
|
+
export function resolveResultType(
|
|
133
|
+
spec: ResultSpec,
|
|
134
|
+
ctx: {
|
|
135
|
+
input?: TypeInfo;
|
|
136
|
+
left?: TypeInfo;
|
|
137
|
+
right?: TypeInfo;
|
|
138
|
+
firstParam?: TypeInfo;
|
|
139
|
+
}
|
|
140
|
+
): TypeInfo {
|
|
141
|
+
if (typeof spec !== 'string') return spec;
|
|
142
|
+
|
|
143
|
+
switch (spec) {
|
|
144
|
+
case 'inputType':
|
|
145
|
+
return ctx.input || { type: 'Any', singleton: false };
|
|
146
|
+
case 'inputTypeSingleton':
|
|
147
|
+
return ctx.input ? { ...ctx.input, singleton: true } : { type: 'Any', singleton: true };
|
|
148
|
+
case 'leftType':
|
|
149
|
+
return ctx.left ? { ...ctx.left, singleton: false } : { type: 'Any', singleton: false };
|
|
150
|
+
case 'rightType':
|
|
151
|
+
return ctx.right ? { ...ctx.right, singleton: false } : { type: 'Any', singleton: false };
|
|
152
|
+
case 'parameterType':
|
|
153
|
+
return ctx.firstParam ? { ...ctx.firstParam, singleton: false } : { type: 'Any', singleton: false };
|
|
154
|
+
default:
|
|
155
|
+
return { type: 'Any', singleton: false };
|
|
156
|
+
}
|
|
157
|
+
}
|