@atomic-ehr/fhirpath 0.0.1 → 0.0.2-canary.261bb9b.20250811212042
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 +62 -56
- package/dist/index.d.ts +368 -99
- package/dist/index.js +3623 -1253
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/analyzer.ts +779 -120
- package/src/boxing.ts +124 -0
- package/src/completion-provider.ts +636 -0
- package/src/cursor-nodes.ts +111 -0
- package/src/errors.ts +247 -0
- package/src/index.ts +97 -14
- package/src/inspect.ts +309 -176
- package/src/interpreter.ts +380 -71
- package/src/lexer.ts +1 -0
- package/src/model-provider.ts +282 -39
- package/src/operations/abs-function.ts +40 -16
- package/src/operations/aggregate-function.ts +21 -10
- package/src/operations/all-function.ts +23 -11
- package/src/operations/allFalse-function.ts +14 -9
- package/src/operations/allTrue-function.ts +14 -9
- package/src/operations/and-operator.ts +21 -12
- package/src/operations/anyFalse-function.ts +11 -7
- package/src/operations/anyTrue-function.ts +11 -7
- package/src/operations/as-operator.ts +2 -1
- package/src/operations/ceiling-function.ts +14 -8
- package/src/operations/children-function.ts +98 -0
- package/src/operations/combine-function.ts +10 -6
- package/src/operations/combine-operator.ts +19 -5
- package/src/operations/contains-function.ts +26 -12
- package/src/operations/contains-operator.ts +15 -7
- package/src/operations/count-function.ts +7 -4
- package/src/operations/defineVariable-function.ts +11 -7
- package/src/operations/descendants-function.ts +64 -0
- package/src/operations/distinct-function.ts +21 -7
- package/src/operations/div-operator.ts +16 -3
- package/src/operations/divide-operator.ts +11 -6
- package/src/operations/dot-operator.ts +4 -2
- package/src/operations/empty-function.ts +7 -4
- package/src/operations/endsWith-function.ts +28 -14
- package/src/operations/equal-operator.ts +19 -7
- package/src/operations/equivalent-operator.ts +16 -11
- package/src/operations/exclude-function.ts +18 -14
- package/src/operations/exists-function.ts +19 -10
- package/src/operations/first-function.ts +12 -7
- package/src/operations/floor-function.ts +14 -8
- package/src/operations/greater-operator.ts +10 -5
- package/src/operations/greater-or-equal-operator.ts +10 -5
- package/src/operations/iif-function.ts +27 -23
- package/src/operations/implies-operator.ts +23 -8
- package/src/operations/in-operator.ts +18 -8
- package/src/operations/index.ts +3 -0
- package/src/operations/indexOf-function.ts +28 -14
- package/src/operations/intersect-function.ts +23 -24
- package/src/operations/is-operator.ts +67 -15
- package/src/operations/isDistinct-function.ts +17 -9
- package/src/operations/join-function.ts +22 -9
- package/src/operations/last-function.ts +12 -7
- package/src/operations/length-function.ts +17 -8
- package/src/operations/less-operator.ts +10 -5
- package/src/operations/less-or-equal-operator.ts +10 -5
- package/src/operations/less-than.ts +26 -2
- package/src/operations/lower-function.ts +17 -8
- package/src/operations/minus-operator.ts +10 -5
- package/src/operations/mod-operator.ts +20 -3
- package/src/operations/multiply-operator.ts +12 -7
- package/src/operations/not-equal-operator.ts +16 -7
- package/src/operations/not-equivalent-operator.ts +16 -11
- package/src/operations/not-function.ts +17 -7
- package/src/operations/ofType-function.ts +139 -0
- package/src/operations/or-operator.ts +21 -12
- package/src/operations/plus-operator.ts +19 -7
- package/src/operations/power-function.ts +27 -13
- package/src/operations/replace-function.ts +33 -14
- package/src/operations/round-function.ts +29 -12
- package/src/operations/select-function.ts +17 -9
- package/src/operations/single-function.ts +10 -6
- package/src/operations/skip-function.ts +18 -9
- package/src/operations/split-function.ts +30 -13
- package/src/operations/sqrt-function.ts +14 -8
- package/src/operations/startsWith-function.ts +26 -12
- package/src/operations/subsetOf-function.ts +20 -15
- package/src/operations/substring-function.ts +43 -24
- package/src/operations/supersetOf-function.ts +20 -15
- package/src/operations/tail-function.ts +8 -4
- package/src/operations/take-function.ts +18 -9
- package/src/operations/toBoolean-function.ts +23 -14
- package/src/operations/toDecimal-function.ts +18 -9
- package/src/operations/toInteger-function.ts +22 -12
- package/src/operations/toString-function.ts +26 -16
- package/src/operations/trace-function.ts +19 -10
- package/src/operations/trim-function.ts +16 -7
- package/src/operations/truncate-function.ts +14 -8
- package/src/operations/unary-minus-operator.ts +23 -13
- package/src/operations/unary-plus-operator.ts +2 -1
- package/src/operations/union-function.ts +25 -28
- package/src/operations/union-operator.ts +2 -1
- package/src/operations/upper-function.ts +17 -8
- package/src/operations/where-function.ts +20 -11
- package/src/operations/xor-operator.ts +9 -4
- package/src/parser.ts +527 -8
- package/src/quantity-value.ts +4 -8
- package/src/registry.ts +147 -5
- package/src/types.ts +42 -19
- package/src/parser-base.ts +0 -400
package/src/analyzer.ts
CHANGED
|
@@ -20,34 +20,53 @@ 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';
|
|
24
|
+
import { isCursorNode, CursorContext } from './cursor-nodes';
|
|
25
|
+
import type { AnyCursorNode } from './cursor-nodes';
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
27
|
+
|
|
28
|
+
export interface AnalyzerOptions {
|
|
29
|
+
cursorMode?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AnalysisResultWithCursor extends AnalysisResult {
|
|
33
|
+
stoppedAtCursor?: boolean;
|
|
34
|
+
cursorContext?: {
|
|
35
|
+
typeBeforeCursor?: TypeInfo;
|
|
36
|
+
expectedType?: TypeInfo;
|
|
37
|
+
cursorNode?: AnyCursorNode;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
37
40
|
|
|
38
41
|
export class Analyzer {
|
|
39
42
|
private diagnostics: Diagnostic[] = [];
|
|
40
|
-
private variables: Set<string> = new Set(['$this', '$index', '$total']);
|
|
43
|
+
private variables: Set<string> = new Set(['$this', '$index', '$total', 'context', 'resource', 'rootResource']);
|
|
41
44
|
private modelProvider?: ModelProvider;
|
|
42
45
|
private userVariableTypes: Map<string, TypeInfo> = new Map();
|
|
46
|
+
private systemVariableTypes: Map<string, TypeInfo> = new Map();
|
|
47
|
+
private cursorMode: boolean = false;
|
|
48
|
+
private stoppedAtCursor: boolean = false;
|
|
49
|
+
private cursorContext?: {
|
|
50
|
+
typeBeforeCursor?: TypeInfo;
|
|
51
|
+
expectedType?: TypeInfo;
|
|
52
|
+
cursorNode?: AnyCursorNode;
|
|
53
|
+
};
|
|
43
54
|
|
|
44
55
|
constructor(modelProvider?: ModelProvider) {
|
|
45
56
|
this.modelProvider = modelProvider;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
analyze(
|
|
59
|
+
async analyze(
|
|
60
|
+
ast: ASTNode,
|
|
61
|
+
userVariables?: Record<string, any>,
|
|
62
|
+
inputType?: TypeInfo,
|
|
63
|
+
options?: AnalyzerOptions
|
|
64
|
+
): Promise<AnalysisResultWithCursor> {
|
|
49
65
|
this.diagnostics = [];
|
|
50
66
|
this.userVariableTypes.clear();
|
|
67
|
+
this.cursorMode = options?.cursorMode ?? false;
|
|
68
|
+
this.stoppedAtCursor = false;
|
|
69
|
+
this.cursorContext = undefined;
|
|
51
70
|
|
|
52
71
|
if (userVariables) {
|
|
53
72
|
Object.keys(userVariables).forEach(name => {
|
|
@@ -61,24 +80,43 @@ export class Analyzer {
|
|
|
61
80
|
}
|
|
62
81
|
|
|
63
82
|
// Annotate AST with type information
|
|
64
|
-
this.annotateAST(ast, inputType);
|
|
83
|
+
await this.annotateAST(ast, inputType);
|
|
65
84
|
|
|
66
|
-
// Perform validation with type checking
|
|
67
|
-
this.
|
|
85
|
+
// Perform validation with type checking (if not stopped at cursor)
|
|
86
|
+
if (!this.stoppedAtCursor) {
|
|
87
|
+
this.visitNode(ast);
|
|
88
|
+
}
|
|
68
89
|
|
|
69
90
|
return {
|
|
70
91
|
diagnostics: this.diagnostics,
|
|
71
|
-
ast
|
|
92
|
+
ast,
|
|
93
|
+
stoppedAtCursor: this.cursorMode ? this.stoppedAtCursor : undefined,
|
|
94
|
+
cursorContext: this.cursorMode ? this.cursorContext : undefined
|
|
72
95
|
};
|
|
73
96
|
}
|
|
74
97
|
|
|
75
98
|
private visitNode(node: ASTNode): void {
|
|
99
|
+
// Check for cursor node in cursor mode
|
|
100
|
+
if (this.cursorMode && isCursorNode(node)) {
|
|
101
|
+
this.stoppedAtCursor = true;
|
|
102
|
+
this.cursorContext = {
|
|
103
|
+
cursorNode: node as AnyCursorNode,
|
|
104
|
+
typeBeforeCursor: (node as any).typeInfo
|
|
105
|
+
};
|
|
106
|
+
return; // Short-circuit
|
|
107
|
+
}
|
|
108
|
+
|
|
76
109
|
// Handle error nodes - process them for diagnostics but don't traverse
|
|
77
110
|
if (node.type === 'Error') {
|
|
78
111
|
// Diagnostics already added in annotateAST
|
|
79
112
|
return;
|
|
80
113
|
}
|
|
81
114
|
|
|
115
|
+
// If we've already stopped at cursor, don't continue
|
|
116
|
+
if (this.stoppedAtCursor) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
82
120
|
switch (node.type) {
|
|
83
121
|
case NodeType.Binary:
|
|
84
122
|
this.visitBinaryOperator(node as BinaryNode);
|
|
@@ -101,10 +139,10 @@ export class Analyzer {
|
|
|
101
139
|
this.visitNode((node as UnaryNode).operand);
|
|
102
140
|
break;
|
|
103
141
|
case NodeType.MembershipTest:
|
|
104
|
-
this.
|
|
142
|
+
this.visitMembershipTest(node as MembershipTestNode);
|
|
105
143
|
break;
|
|
106
144
|
case NodeType.TypeCast:
|
|
107
|
-
this.
|
|
145
|
+
this.visitTypeCast(node as TypeCastNode);
|
|
108
146
|
break;
|
|
109
147
|
case NodeType.Variable:
|
|
110
148
|
this.validateVariable((node as VariableNode).name, node);
|
|
@@ -120,22 +158,84 @@ export class Analyzer {
|
|
|
120
158
|
private visitBinaryOperator(node: BinaryNode): void {
|
|
121
159
|
this.visitNode(node.left);
|
|
122
160
|
|
|
161
|
+
// Track defineVariable for validation - collect all variables defined in the chain
|
|
162
|
+
if (node.operator === '.') {
|
|
163
|
+
const definedVars = this.collectDefinedVariables(node.left);
|
|
164
|
+
if (definedVars.size > 0) {
|
|
165
|
+
// Track which variables were already known
|
|
166
|
+
const previouslyKnown = new Set<string>();
|
|
167
|
+
definedVars.forEach(varName => {
|
|
168
|
+
if (this.variables.has(varName)) {
|
|
169
|
+
previouslyKnown.add(varName);
|
|
170
|
+
}
|
|
171
|
+
this.variables.add(varName);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Visit right side with new variables in scope
|
|
175
|
+
this.visitNode(node.right);
|
|
176
|
+
|
|
177
|
+
// Restore previous state
|
|
178
|
+
definedVars.forEach(varName => {
|
|
179
|
+
if (!previouslyKnown.has(varName)) {
|
|
180
|
+
this.variables.delete(varName);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
123
187
|
// Special handling for dot operator with function on right side
|
|
124
188
|
if (node.operator === '.' && node.right.type === NodeType.Function) {
|
|
125
189
|
const funcNode = node.right as FunctionNode;
|
|
126
190
|
if (funcNode.name.type === NodeType.Identifier) {
|
|
127
191
|
const funcName = (funcNode.name as IdentifierNode).name;
|
|
128
192
|
const func = registry.getFunction(funcName);
|
|
129
|
-
if (func && func.
|
|
130
|
-
if
|
|
193
|
+
if (func && func.signatures && func.signatures.length > 0 && node.left.typeInfo) {
|
|
194
|
+
// Check if any signature matches the input type
|
|
195
|
+
let matchFound = false;
|
|
196
|
+
let expectedTypes: string[] = [];
|
|
197
|
+
|
|
198
|
+
for (const signature of func.signatures) {
|
|
199
|
+
if (signature.input) {
|
|
200
|
+
if (this.isTypeCompatible(node.left.typeInfo, signature.input)) {
|
|
201
|
+
matchFound = true;
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
expectedTypes.push(this.typeToString(signature.input));
|
|
205
|
+
} else {
|
|
206
|
+
// If any signature has no input constraint, it matches
|
|
207
|
+
matchFound = true;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!matchFound) {
|
|
131
213
|
const inputTypeStr = this.typeToString(node.left.typeInfo);
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
214
|
+
const firstSignature = func.signatures[0];
|
|
215
|
+
|
|
216
|
+
if (!firstSignature) return;
|
|
217
|
+
|
|
218
|
+
// Check if this is specifically a singleton/collection mismatch
|
|
219
|
+
const inputIsCollection = !node.left.typeInfo.singleton;
|
|
220
|
+
const expectedIsSingleton = firstSignature.input?.singleton;
|
|
221
|
+
|
|
222
|
+
// Check if the base types are compatible (same type or subtype)
|
|
223
|
+
const typesCompatible = firstSignature.input && (
|
|
224
|
+
node.left.typeInfo.type === firstSignature.input.type ||
|
|
225
|
+
this.isSubtypeOf(node.left.typeInfo.type, firstSignature.input.type)
|
|
138
226
|
);
|
|
227
|
+
|
|
228
|
+
if (inputIsCollection && expectedIsSingleton && typesCompatible) {
|
|
229
|
+
// Compatible base types but collection vs singleton mismatch
|
|
230
|
+
this.diagnostics.push(
|
|
231
|
+
toDiagnostic(Errors.singletonTypeRequired(funcName, inputTypeStr, funcNode.range))
|
|
232
|
+
);
|
|
233
|
+
} else {
|
|
234
|
+
// Function received invalid operand type - report as runtime error
|
|
235
|
+
this.diagnostics.push(
|
|
236
|
+
toDiagnostic(Errors.invalidOperandType(funcName + '()', inputTypeStr, funcNode.range))
|
|
237
|
+
);
|
|
238
|
+
}
|
|
139
239
|
}
|
|
140
240
|
}
|
|
141
241
|
}
|
|
@@ -150,7 +250,9 @@ export class Analyzer {
|
|
|
150
250
|
|
|
151
251
|
const op = registry.getOperatorDefinition(node.operator);
|
|
152
252
|
if (!op) {
|
|
153
|
-
this.
|
|
253
|
+
this.diagnostics.push(
|
|
254
|
+
toDiagnostic(Errors.unknownOperator(node.operator, node.range))
|
|
255
|
+
);
|
|
154
256
|
return;
|
|
155
257
|
}
|
|
156
258
|
|
|
@@ -167,20 +269,87 @@ export class Analyzer {
|
|
|
167
269
|
private visitFunctionCall(node: FunctionNode): void {
|
|
168
270
|
if (node.name.type === NodeType.Identifier) {
|
|
169
271
|
const funcName = (node.name as IdentifierNode).name;
|
|
272
|
+
|
|
273
|
+
// Check if this is a type operation that requires ModelProvider
|
|
274
|
+
if (funcName === 'ofType' && !this.modelProvider) {
|
|
275
|
+
// Check if the type argument is a primitive type
|
|
276
|
+
const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
|
|
277
|
+
let isPrimitive = false;
|
|
278
|
+
|
|
279
|
+
if (node.arguments.length > 0) {
|
|
280
|
+
const typeArg = node.arguments[0]!;
|
|
281
|
+
if (typeArg.type === NodeType.Identifier) {
|
|
282
|
+
isPrimitive = primitiveTypes.includes((typeArg as IdentifierNode).name);
|
|
283
|
+
} else if ((typeArg as any).type === NodeType.TypeOrIdentifier || (typeArg as any).type === NodeType.TypeReference) {
|
|
284
|
+
isPrimitive = primitiveTypes.includes((typeArg as any).name);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!isPrimitive) {
|
|
289
|
+
this.diagnostics.push(
|
|
290
|
+
toDiagnostic(Errors.modelProviderRequired('ofType', node.range))
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Check ofType with union types
|
|
296
|
+
if (funcName === 'ofType' && node.typeInfo) {
|
|
297
|
+
const inputType = node.typeInfo;
|
|
298
|
+
if (node.arguments.length > 0 && inputType.modelContext &&
|
|
299
|
+
typeof inputType.modelContext === 'object' &&
|
|
300
|
+
'isUnion' in inputType.modelContext &&
|
|
301
|
+
inputType.modelContext.isUnion &&
|
|
302
|
+
'choices' in inputType.modelContext &&
|
|
303
|
+
Array.isArray(inputType.modelContext.choices)) {
|
|
304
|
+
|
|
305
|
+
// Extract target type from argument
|
|
306
|
+
let targetType: string | undefined;
|
|
307
|
+
const typeArg = node.arguments[0]!;
|
|
308
|
+
if (typeArg.type === NodeType.Identifier) {
|
|
309
|
+
targetType = (typeArg as IdentifierNode).name;
|
|
310
|
+
} else if ((typeArg as any).type === NodeType.TypeOrIdentifier || (typeArg as any).type === NodeType.TypeReference) {
|
|
311
|
+
targetType = (typeArg as any).name;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (targetType) {
|
|
315
|
+
const validChoice = inputType.modelContext.choices.find((choice: any) =>
|
|
316
|
+
choice.type === targetType || choice.code === targetType
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
if (!validChoice) {
|
|
320
|
+
this.diagnostics.push({
|
|
321
|
+
severity: DiagnosticSeverity.Warning,
|
|
322
|
+
code: 'invalid-type-filter',
|
|
323
|
+
message: `Type '${targetType}' is not present in the union type. Available types: ${
|
|
324
|
+
inputType.modelContext.choices.map((c: any) => c.type || c.code).join(', ')
|
|
325
|
+
}`,
|
|
326
|
+
range: node.range
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
170
333
|
const func = registry.getFunction(funcName);
|
|
171
334
|
|
|
172
335
|
if (!func) {
|
|
173
|
-
this.
|
|
336
|
+
this.diagnostics.push(
|
|
337
|
+
toDiagnostic(Errors.unknownFunction(funcName, node.range))
|
|
338
|
+
);
|
|
174
339
|
} else {
|
|
175
340
|
// Check argument count based on signature
|
|
176
|
-
const params = func.
|
|
341
|
+
const params = func.signatures?.[0]?.parameters || [];
|
|
177
342
|
const requiredParams = params.filter(p => !p.optional).length;
|
|
178
343
|
const maxParams = params.length;
|
|
179
344
|
|
|
180
345
|
if (node.arguments.length < requiredParams) {
|
|
181
|
-
this.
|
|
346
|
+
this.diagnostics.push(
|
|
347
|
+
toDiagnostic(Errors.wrongArgumentCount(funcName, requiredParams, node.arguments.length, node.range))
|
|
348
|
+
);
|
|
182
349
|
} else if (node.arguments.length > maxParams) {
|
|
183
|
-
this.
|
|
350
|
+
this.diagnostics.push(
|
|
351
|
+
toDiagnostic(Errors.wrongArgumentCount(funcName, maxParams, node.arguments.length, node.range))
|
|
352
|
+
);
|
|
184
353
|
}
|
|
185
354
|
|
|
186
355
|
// Type check arguments if we have type information
|
|
@@ -193,32 +362,217 @@ export class Analyzer {
|
|
|
193
362
|
node.arguments.forEach(arg => this.visitNode(arg));
|
|
194
363
|
}
|
|
195
364
|
|
|
365
|
+
private visitMembershipTest(node: MembershipTestNode): void {
|
|
366
|
+
// Check if ModelProvider is required
|
|
367
|
+
// Basic primitive types can be checked without ModelProvider
|
|
368
|
+
const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
|
|
369
|
+
if (!this.modelProvider && !primitiveTypes.includes(node.targetType)) {
|
|
370
|
+
this.diagnostics.push(
|
|
371
|
+
toDiagnostic(Errors.modelProviderRequired('is', node.range))
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Check 'is' with union types
|
|
376
|
+
if (node.expression.typeInfo) {
|
|
377
|
+
const leftType = node.expression.typeInfo;
|
|
378
|
+
if (leftType.modelContext &&
|
|
379
|
+
typeof leftType.modelContext === 'object' &&
|
|
380
|
+
'isUnion' in leftType.modelContext &&
|
|
381
|
+
leftType.modelContext.isUnion &&
|
|
382
|
+
'choices' in leftType.modelContext &&
|
|
383
|
+
Array.isArray(leftType.modelContext.choices)) {
|
|
384
|
+
|
|
385
|
+
const targetTypeName = node.targetType;
|
|
386
|
+
const validChoice = leftType.modelContext.choices.find((choice: any) =>
|
|
387
|
+
choice.type === targetTypeName || choice.code === targetTypeName
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
if (!validChoice) {
|
|
391
|
+
this.diagnostics.push({
|
|
392
|
+
severity: DiagnosticSeverity.Warning,
|
|
393
|
+
code: 'invalid-type-test',
|
|
394
|
+
message: `Type test 'is ${targetTypeName}' will always be false. Type '${targetTypeName}' is not in the union. Available types: ${
|
|
395
|
+
leftType.modelContext.choices.map((c: any) => c.type || c.code).join(', ')
|
|
396
|
+
}`,
|
|
397
|
+
range: node.range
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
this.visitNode(node.expression);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private visitTypeCast(node: TypeCastNode): void {
|
|
407
|
+
// Check if ModelProvider is required
|
|
408
|
+
// Basic primitive types can be checked without ModelProvider
|
|
409
|
+
const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
|
|
410
|
+
if (!this.modelProvider && !primitiveTypes.includes(node.targetType)) {
|
|
411
|
+
this.diagnostics.push(
|
|
412
|
+
toDiagnostic(Errors.modelProviderRequired('as', node.range))
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Check 'as' with union types
|
|
417
|
+
if (node.expression.typeInfo) {
|
|
418
|
+
const leftType = node.expression.typeInfo;
|
|
419
|
+
if (leftType.modelContext &&
|
|
420
|
+
typeof leftType.modelContext === 'object' &&
|
|
421
|
+
'isUnion' in leftType.modelContext &&
|
|
422
|
+
leftType.modelContext.isUnion &&
|
|
423
|
+
'choices' in leftType.modelContext &&
|
|
424
|
+
Array.isArray(leftType.modelContext.choices)) {
|
|
425
|
+
|
|
426
|
+
const targetTypeName = node.targetType;
|
|
427
|
+
const validChoice = leftType.modelContext.choices.find((choice: any) =>
|
|
428
|
+
choice.type === targetTypeName || choice.code === targetTypeName
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
if (!validChoice) {
|
|
432
|
+
this.diagnostics.push({
|
|
433
|
+
severity: DiagnosticSeverity.Warning,
|
|
434
|
+
code: 'invalid-type-cast',
|
|
435
|
+
message: `Type cast 'as ${targetTypeName}' may fail. Type '${targetTypeName}' is not guaranteed in the union. Available types: ${
|
|
436
|
+
leftType.modelContext.choices.map((c: any) => c.type || c.code).join(', ')
|
|
437
|
+
}`,
|
|
438
|
+
range: node.range
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
this.visitNode(node.expression);
|
|
445
|
+
}
|
|
446
|
+
|
|
196
447
|
// Unified variable validation to eliminate duplication
|
|
197
448
|
private validateVariable(name: string, node: ASTNode): void {
|
|
198
449
|
if (name.startsWith('$')) {
|
|
199
450
|
if (!this.variables.has(name)) {
|
|
200
|
-
this.
|
|
451
|
+
this.diagnostics.push(
|
|
452
|
+
toDiagnostic(Errors.unknownVariable(name, node.range))
|
|
453
|
+
);
|
|
201
454
|
}
|
|
202
455
|
} else if (name.startsWith('%')) {
|
|
203
456
|
const varName = name.substring(1);
|
|
204
457
|
if (!this.variables.has(varName)) {
|
|
205
|
-
this.
|
|
458
|
+
this.diagnostics.push(
|
|
459
|
+
toDiagnostic(Errors.unknownUserVariable(name, node.range))
|
|
460
|
+
);
|
|
206
461
|
}
|
|
207
462
|
}
|
|
208
463
|
}
|
|
209
|
-
|
|
210
|
-
private
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
464
|
+
|
|
465
|
+
private collectDefinedVariables(node: ASTNode): Set<string> {
|
|
466
|
+
const vars = new Set<string>();
|
|
467
|
+
|
|
468
|
+
// If this is a defineVariable call, extract the variable name
|
|
469
|
+
if (node.type === NodeType.Function) {
|
|
470
|
+
const funcNode = node as FunctionNode;
|
|
471
|
+
if (funcNode.name.type === NodeType.Identifier &&
|
|
472
|
+
(funcNode.name as IdentifierNode).name === 'defineVariable' &&
|
|
473
|
+
funcNode.arguments.length >= 1) {
|
|
474
|
+
const nameArg = funcNode.arguments[0];
|
|
475
|
+
if (nameArg && nameArg.type === NodeType.Literal && nameArg.valueType === 'string') {
|
|
476
|
+
vars.add(nameArg.value as string);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// If this is a binary dot operator, collect from left side recursively
|
|
482
|
+
if (node.type === NodeType.Binary) {
|
|
483
|
+
const binaryNode = node as BinaryNode;
|
|
484
|
+
if (binaryNode.operator === '.') {
|
|
485
|
+
// Collect from left side
|
|
486
|
+
const leftVars = this.collectDefinedVariables(binaryNode.left);
|
|
487
|
+
leftVars.forEach(v => vars.add(v));
|
|
488
|
+
|
|
489
|
+
// Check if right side is also defineVariable
|
|
490
|
+
if (binaryNode.right.type === NodeType.Function) {
|
|
491
|
+
const rightFunc = binaryNode.right as FunctionNode;
|
|
492
|
+
if (rightFunc.name.type === NodeType.Identifier &&
|
|
493
|
+
(rightFunc.name as IdentifierNode).name === 'defineVariable' &&
|
|
494
|
+
rightFunc.arguments.length >= 1) {
|
|
495
|
+
const nameArg = rightFunc.arguments[0];
|
|
496
|
+
if (nameArg && nameArg.type === NodeType.Literal && nameArg.valueType === 'string') {
|
|
497
|
+
vars.add(nameArg.value as string);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return vars;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private collectDefinedVariablesWithTypes(node: ASTNode): Map<string, TypeInfo> {
|
|
508
|
+
const varsWithTypes = new Map<string, TypeInfo>();
|
|
509
|
+
|
|
510
|
+
// If this is a defineVariable call, extract the variable name and type
|
|
511
|
+
if (node.type === NodeType.Function) {
|
|
512
|
+
const funcNode = node as FunctionNode;
|
|
513
|
+
if (funcNode.name.type === NodeType.Identifier &&
|
|
514
|
+
(funcNode.name as IdentifierNode).name === 'defineVariable' &&
|
|
515
|
+
funcNode.arguments.length >= 1) {
|
|
516
|
+
const nameArg = funcNode.arguments[0];
|
|
517
|
+
if (nameArg && nameArg.type === NodeType.Literal && nameArg.valueType === 'string') {
|
|
518
|
+
const varName = nameArg.value as string;
|
|
519
|
+
let varType: TypeInfo;
|
|
520
|
+
|
|
521
|
+
if (funcNode.arguments.length >= 2 && funcNode.arguments[1]!.typeInfo) {
|
|
522
|
+
// Has value expression - use its type
|
|
523
|
+
varType = funcNode.arguments[1]!.typeInfo;
|
|
524
|
+
} else if (node.typeInfo) {
|
|
525
|
+
// No value expression - uses input as value (defineVariable returns input)
|
|
526
|
+
varType = node.typeInfo;
|
|
527
|
+
} else {
|
|
528
|
+
varType = { type: 'Any', singleton: false };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
varsWithTypes.set(varName, varType);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// If this is a binary dot operator, collect from entire chain
|
|
537
|
+
if (node.type === NodeType.Binary) {
|
|
538
|
+
const binaryNode = node as BinaryNode;
|
|
539
|
+
if (binaryNode.operator === '.') {
|
|
540
|
+
// Collect from left side recursively
|
|
541
|
+
const leftVars = this.collectDefinedVariablesWithTypes(binaryNode.left);
|
|
542
|
+
leftVars.forEach((type, name) => varsWithTypes.set(name, type));
|
|
543
|
+
|
|
544
|
+
// Check if right side is also defineVariable
|
|
545
|
+
if (binaryNode.right.type === NodeType.Function) {
|
|
546
|
+
const rightFunc = binaryNode.right as FunctionNode;
|
|
547
|
+
if (rightFunc.name.type === NodeType.Identifier &&
|
|
548
|
+
(rightFunc.name as IdentifierNode).name === 'defineVariable' &&
|
|
549
|
+
rightFunc.arguments.length >= 1) {
|
|
550
|
+
const nameArg = rightFunc.arguments[0];
|
|
551
|
+
if (nameArg && nameArg.type === NodeType.Literal && nameArg.valueType === 'string') {
|
|
552
|
+
const varName = nameArg.value as string;
|
|
553
|
+
let varType: TypeInfo;
|
|
554
|
+
|
|
555
|
+
if (rightFunc.arguments.length >= 2 && rightFunc.arguments[1]!.typeInfo) {
|
|
556
|
+
varType = rightFunc.arguments[1]!.typeInfo;
|
|
557
|
+
} else if (binaryNode.typeInfo) {
|
|
558
|
+
varType = binaryNode.typeInfo;
|
|
559
|
+
} else {
|
|
560
|
+
varType = { type: 'Any', singleton: false };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
varsWithTypes.set(varName, varType);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return varsWithTypes;
|
|
218
571
|
}
|
|
219
572
|
|
|
573
|
+
|
|
220
574
|
// Type inference methods
|
|
221
|
-
private inferType(node: ASTNode, inputType?: TypeInfo): TypeInfo {
|
|
575
|
+
private async inferType(node: ASTNode, inputType?: TypeInfo): Promise<TypeInfo> {
|
|
222
576
|
// Handle error nodes
|
|
223
577
|
if (node.type === 'Error') {
|
|
224
578
|
return this.inferErrorNodeType(node as ErrorNode, inputType);
|
|
@@ -229,31 +583,31 @@ export class Analyzer {
|
|
|
229
583
|
return this.inferLiteralType(node as LiteralNode);
|
|
230
584
|
|
|
231
585
|
case NodeType.Binary:
|
|
232
|
-
return this.inferBinaryType(node as BinaryNode, inputType);
|
|
586
|
+
return await this.inferBinaryType(node as BinaryNode, inputType);
|
|
233
587
|
|
|
234
588
|
case NodeType.Unary:
|
|
235
589
|
return this.inferUnaryType(node as UnaryNode);
|
|
236
590
|
|
|
237
591
|
case NodeType.Function:
|
|
238
|
-
return this.inferFunctionType(node as FunctionNode, inputType);
|
|
592
|
+
return await this.inferFunctionType(node as FunctionNode, inputType);
|
|
239
593
|
|
|
240
594
|
case NodeType.Identifier:
|
|
241
|
-
return this.inferIdentifierType(node as IdentifierNode, inputType);
|
|
595
|
+
return await this.inferIdentifierType(node as IdentifierNode, inputType);
|
|
242
596
|
|
|
243
597
|
case NodeType.Variable:
|
|
244
598
|
return this.inferVariableType(node as VariableNode);
|
|
245
599
|
|
|
246
600
|
case NodeType.Collection:
|
|
247
|
-
return this.inferCollectionType(node as CollectionNode);
|
|
601
|
+
return await this.inferCollectionType(node as CollectionNode);
|
|
248
602
|
|
|
249
603
|
case NodeType.TypeCast:
|
|
250
|
-
return this.inferTypeCastType(node as TypeCastNode);
|
|
604
|
+
return await this.inferTypeCastType(node as TypeCastNode);
|
|
251
605
|
|
|
252
606
|
case NodeType.MembershipTest:
|
|
253
607
|
return { type: 'Boolean', singleton: true };
|
|
254
608
|
|
|
255
609
|
case NodeType.TypeOrIdentifier:
|
|
256
|
-
return this.inferTypeOrIdentifierType(node as TypeOrIdentifierNode, inputType);
|
|
610
|
+
return await this.inferTypeOrIdentifierType(node as TypeOrIdentifierNode, inputType);
|
|
257
611
|
|
|
258
612
|
default:
|
|
259
613
|
return { type: 'Any', singleton: false };
|
|
@@ -291,7 +645,7 @@ export class Analyzer {
|
|
|
291
645
|
}
|
|
292
646
|
}
|
|
293
647
|
|
|
294
|
-
private inferBinaryType(node: BinaryNode, inputType?: TypeInfo): TypeInfo {
|
|
648
|
+
private async inferBinaryType(node: BinaryNode, inputType?: TypeInfo): Promise<TypeInfo> {
|
|
295
649
|
const operator = registry.getOperatorDefinition(node.operator);
|
|
296
650
|
if (!operator) {
|
|
297
651
|
return { type: 'Any', singleton: false };
|
|
@@ -299,12 +653,12 @@ export class Analyzer {
|
|
|
299
653
|
|
|
300
654
|
// For navigation (dot operator), we need special handling
|
|
301
655
|
if (node.operator === '.') {
|
|
302
|
-
return this.inferNavigationType(node, inputType);
|
|
656
|
+
return await this.inferNavigationType(node, inputType);
|
|
303
657
|
}
|
|
304
658
|
|
|
305
659
|
// Infer types of operands
|
|
306
|
-
const leftType = this.inferType(node.left, inputType);
|
|
307
|
-
const rightType = this.inferType(node.right, inputType);
|
|
660
|
+
const leftType = await this.inferType(node.left, inputType);
|
|
661
|
+
const rightType = await this.inferType(node.right, inputType);
|
|
308
662
|
|
|
309
663
|
// Find matching signature
|
|
310
664
|
for (const sig of operator.signatures) {
|
|
@@ -319,12 +673,12 @@ export class Analyzer {
|
|
|
319
673
|
return this.resolveResultType(defaultResult, inputType, leftType, rightType);
|
|
320
674
|
}
|
|
321
675
|
|
|
322
|
-
private inferNavigationType(node: BinaryNode, inputType?: TypeInfo): TypeInfo {
|
|
323
|
-
const leftType = this.inferType(node.left, inputType);
|
|
676
|
+
private async inferNavigationType(node: BinaryNode, inputType?: TypeInfo): Promise<TypeInfo> {
|
|
677
|
+
const leftType = await this.inferType(node.left, inputType);
|
|
324
678
|
|
|
325
679
|
// If the right side is a function, return the function's type
|
|
326
680
|
if (node.right.type === NodeType.Function) {
|
|
327
|
-
return this.inferType(node.right, leftType);
|
|
681
|
+
return await this.inferType(node.right, leftType);
|
|
328
682
|
}
|
|
329
683
|
|
|
330
684
|
// If we have a model provider and the right side is an identifier
|
|
@@ -332,7 +686,7 @@ export class Analyzer {
|
|
|
332
686
|
const propertyName = (node.right as IdentifierNode).name;
|
|
333
687
|
|
|
334
688
|
// Use getElementType to navigate the property
|
|
335
|
-
const resultType = this.modelProvider.getElementType(leftType, propertyName);
|
|
689
|
+
const resultType = await this.modelProvider.getElementType(leftType, propertyName);
|
|
336
690
|
if (resultType) {
|
|
337
691
|
return resultType;
|
|
338
692
|
}
|
|
@@ -341,11 +695,8 @@ export class Analyzer {
|
|
|
341
695
|
// Skip diagnostics for union types - they may have dynamic properties
|
|
342
696
|
if (leftType.namespace && leftType.name && leftType.modelContext &&
|
|
343
697
|
!(leftType.modelContext as any).isUnion) {
|
|
344
|
-
this.
|
|
345
|
-
|
|
346
|
-
`Unknown property '${propertyName}' on type ${leftType.namespace}.${leftType.name}`,
|
|
347
|
-
node.right,
|
|
348
|
-
'UNKNOWN_PROPERTY'
|
|
698
|
+
this.diagnostics.push(
|
|
699
|
+
toDiagnostic(Errors.unknownProperty(propertyName, `${leftType.namespace}.${leftType.name}`, node.right.range))
|
|
349
700
|
);
|
|
350
701
|
}
|
|
351
702
|
}
|
|
@@ -369,7 +720,7 @@ export class Analyzer {
|
|
|
369
720
|
return { type: 'Any', singleton: false };
|
|
370
721
|
}
|
|
371
722
|
|
|
372
|
-
private inferFunctionType(node: FunctionNode, inputType?: TypeInfo): TypeInfo {
|
|
723
|
+
private async inferFunctionType(node: FunctionNode, inputType?: TypeInfo): Promise<TypeInfo> {
|
|
373
724
|
if (node.name.type !== NodeType.Identifier) {
|
|
374
725
|
return { type: 'Any', singleton: false };
|
|
375
726
|
}
|
|
@@ -381,28 +732,112 @@ export class Analyzer {
|
|
|
381
732
|
return { type: 'Any', singleton: false };
|
|
382
733
|
}
|
|
383
734
|
|
|
735
|
+
// Special handling for iif function
|
|
736
|
+
if (funcName === 'iif') {
|
|
737
|
+
// iif returns the common type of the true and false branches
|
|
738
|
+
if (node.arguments.length >= 2) {
|
|
739
|
+
const trueBranchType = await this.inferType(node.arguments[1]!, inputType);
|
|
740
|
+
if (node.arguments.length >= 3) {
|
|
741
|
+
const falseBranchType = await this.inferType(node.arguments[2]!, inputType);
|
|
742
|
+
// If both branches have the same type, use that
|
|
743
|
+
if (trueBranchType.type === falseBranchType.type &&
|
|
744
|
+
trueBranchType.singleton === falseBranchType.singleton) {
|
|
745
|
+
return trueBranchType;
|
|
746
|
+
}
|
|
747
|
+
// If types are the same but singleton differs, return as collection
|
|
748
|
+
if (trueBranchType.type === falseBranchType.type) {
|
|
749
|
+
// One is singleton, one is collection - result must be collection
|
|
750
|
+
return { type: trueBranchType.type, singleton: false };
|
|
751
|
+
}
|
|
752
|
+
// Otherwise, check if one is a subtype of the other
|
|
753
|
+
if (this.isTypeCompatible(trueBranchType, falseBranchType)) {
|
|
754
|
+
return falseBranchType;
|
|
755
|
+
}
|
|
756
|
+
if (this.isTypeCompatible(falseBranchType, trueBranchType)) {
|
|
757
|
+
return trueBranchType;
|
|
758
|
+
}
|
|
759
|
+
} else {
|
|
760
|
+
// Only true branch, result can be that type or empty
|
|
761
|
+
return { ...trueBranchType, singleton: false };
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return { type: 'Any', singleton: false };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Special handling for defineVariable function
|
|
768
|
+
if (funcName === 'defineVariable') {
|
|
769
|
+
// defineVariable returns its input type unchanged
|
|
770
|
+
return inputType || { type: 'Any', singleton: false };
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Special handling for aggregate function
|
|
774
|
+
if (funcName === 'aggregate') {
|
|
775
|
+
// If init parameter is provided, use its type to infer result type
|
|
776
|
+
if (node.arguments.length >= 2) {
|
|
777
|
+
const initType = await this.inferType(node.arguments[1]!, inputType);
|
|
778
|
+
// The result type is the same as init type
|
|
779
|
+
return initType;
|
|
780
|
+
}
|
|
781
|
+
// Without init, we can't fully infer the type without running annotation
|
|
782
|
+
// This is a limitation - the actual type will be set during annotateAST
|
|
783
|
+
if (node.arguments.length >= 1) {
|
|
784
|
+
// We could try to infer, but it would require setting up system variables
|
|
785
|
+
// For now, return Any and let annotateAST handle proper typing
|
|
786
|
+
return { type: 'Any', singleton: false };
|
|
787
|
+
}
|
|
788
|
+
// No arguments at all
|
|
789
|
+
return { type: 'Any', singleton: false };
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Special handling for children function
|
|
793
|
+
if (funcName === 'children') {
|
|
794
|
+
if (inputType && this.modelProvider && 'getChildrenType' in this.modelProvider) {
|
|
795
|
+
const childrenType = await this.modelProvider.getChildrenType(inputType);
|
|
796
|
+
if (childrenType) {
|
|
797
|
+
return childrenType;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
// Fallback to Any collection
|
|
801
|
+
return { type: 'Any', singleton: false };
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Special handling for descendants function
|
|
805
|
+
// Returns Any type due to combinatorial explosion of possible types
|
|
806
|
+
if (funcName === 'descendants') {
|
|
807
|
+
return { type: 'Any', singleton: false };
|
|
808
|
+
}
|
|
809
|
+
|
|
384
810
|
// Special handling for functions with dynamic result types
|
|
385
|
-
|
|
811
|
+
// Use first matching signature's result type
|
|
812
|
+
const matchingSignature = func.signatures?.find(sig =>
|
|
813
|
+
!sig.input || !inputType || this.isTypeCompatible(inputType, sig.input)
|
|
814
|
+
) || func.signatures?.[0];
|
|
815
|
+
|
|
816
|
+
if (!matchingSignature) {
|
|
817
|
+
return { type: 'Any', singleton: false };
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (matchingSignature.result === 'inputType') {
|
|
386
821
|
// Functions like where() return the same type as input but always as collection
|
|
387
822
|
return inputType ? { ...inputType, singleton: false } : { type: 'Any', singleton: false };
|
|
388
|
-
} else if (
|
|
823
|
+
} else if (matchingSignature.result === 'inputTypeSingleton') {
|
|
389
824
|
// Functions like first(), last() return the same type as input but as singleton
|
|
390
825
|
return inputType ? { ...inputType, singleton: true } : { type: 'Any', singleton: true };
|
|
391
|
-
} else if (
|
|
826
|
+
} else if (matchingSignature.result === 'parameterType' && node.arguments.length > 0) {
|
|
392
827
|
// Functions like select() return the type of the first parameter expression as collection
|
|
393
|
-
const paramType = this.inferType(node.arguments[0]!, inputType);
|
|
828
|
+
const paramType = await this.inferType(node.arguments[0]!, inputType);
|
|
394
829
|
return { ...paramType, singleton: false };
|
|
395
|
-
} else if (typeof
|
|
396
|
-
return
|
|
830
|
+
} else if (typeof matchingSignature.result === 'object') {
|
|
831
|
+
return matchingSignature.result;
|
|
397
832
|
}
|
|
398
833
|
|
|
399
834
|
return { type: 'Any', singleton: false };
|
|
400
835
|
}
|
|
401
836
|
|
|
402
|
-
private inferIdentifierType(node: IdentifierNode, inputType?: TypeInfo): TypeInfo {
|
|
837
|
+
private async inferIdentifierType(node: IdentifierNode, inputType?: TypeInfo): Promise<TypeInfo> {
|
|
403
838
|
// First, try to navigate from input type (most common case)
|
|
404
839
|
if (inputType && this.modelProvider) {
|
|
405
|
-
const elementType = this.modelProvider.getElementType(inputType, node.name);
|
|
840
|
+
const elementType = await this.modelProvider.getElementType(inputType, node.name);
|
|
406
841
|
if (elementType) {
|
|
407
842
|
return elementType;
|
|
408
843
|
}
|
|
@@ -410,8 +845,11 @@ export class Analyzer {
|
|
|
410
845
|
|
|
411
846
|
// Only check if it's a type name if it starts with uppercase (FHIR convention)
|
|
412
847
|
// or if there's no input type context
|
|
413
|
-
|
|
414
|
-
|
|
848
|
+
// Skip common FHIRPath keywords and function names that aren't types
|
|
849
|
+
const fhirPathKeywords = ['Boolean', 'String', 'Integer', 'Decimal', 'Date', 'DateTime', 'Time', 'Quantity', 'ofType'];
|
|
850
|
+
if (this.modelProvider && (!inputType || /^[A-Z]/.test(node.name)) && !fhirPathKeywords.includes(node.name)) {
|
|
851
|
+
// Try to get type from model provider
|
|
852
|
+
const typeInfo = await this.modelProvider.getType(node.name);
|
|
415
853
|
if (typeInfo) {
|
|
416
854
|
return typeInfo;
|
|
417
855
|
}
|
|
@@ -420,20 +858,23 @@ export class Analyzer {
|
|
|
420
858
|
return { type: 'Any', singleton: false };
|
|
421
859
|
}
|
|
422
860
|
|
|
423
|
-
private inferTypeOrIdentifierType(node: TypeOrIdentifierNode, inputType?: TypeInfo): TypeInfo {
|
|
861
|
+
private async inferTypeOrIdentifierType(node: TypeOrIdentifierNode, inputType?: TypeInfo): Promise<TypeInfo> {
|
|
424
862
|
// TypeOrIdentifier can be either a type name or a property navigation
|
|
425
863
|
|
|
426
864
|
// First, try navigation from input type (most common case)
|
|
427
865
|
if (inputType && this.modelProvider) {
|
|
428
|
-
const elementType = this.modelProvider.getElementType(inputType, node.name);
|
|
866
|
+
const elementType = await this.modelProvider.getElementType(inputType, node.name);
|
|
429
867
|
if (elementType) {
|
|
430
868
|
return elementType;
|
|
431
869
|
}
|
|
432
870
|
}
|
|
433
871
|
|
|
434
872
|
// Then check if it's a type name (only for uppercase names or no input context)
|
|
435
|
-
|
|
436
|
-
|
|
873
|
+
// Skip common FHIRPath keywords and function names that aren't types
|
|
874
|
+
const fhirPathKeywords = ['Boolean', 'String', 'Integer', 'Decimal', 'Date', 'DateTime', 'Time', 'Quantity', 'ofType'];
|
|
875
|
+
if (this.modelProvider && (!inputType || /^[A-Z]/.test(node.name)) && !fhirPathKeywords.includes(node.name)) {
|
|
876
|
+
// Try to get type from model provider
|
|
877
|
+
const typeInfo = await this.modelProvider.getType(node.name);
|
|
437
878
|
if (typeInfo) {
|
|
438
879
|
return typeInfo;
|
|
439
880
|
}
|
|
@@ -443,11 +884,28 @@ export class Analyzer {
|
|
|
443
884
|
}
|
|
444
885
|
|
|
445
886
|
private inferVariableType(node: VariableNode): TypeInfo {
|
|
446
|
-
//
|
|
447
|
-
if (node.name
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
887
|
+
// System variables - check temporary context
|
|
888
|
+
if (node.name.startsWith('$')) {
|
|
889
|
+
const systemType = this.systemVariableTypes.get(node.name);
|
|
890
|
+
if (systemType) {
|
|
891
|
+
return systemType;
|
|
892
|
+
}
|
|
893
|
+
// Fallback defaults for system variables
|
|
894
|
+
switch (node.name) {
|
|
895
|
+
case '$this':
|
|
896
|
+
return { type: 'Any', singleton: false };
|
|
897
|
+
case '$index':
|
|
898
|
+
return { type: 'Integer', singleton: true };
|
|
899
|
+
case '$total':
|
|
900
|
+
return { type: 'Any', singleton: false };
|
|
901
|
+
default:
|
|
902
|
+
return { type: 'Any', singleton: false };
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Special FHIRPath environment variables
|
|
907
|
+
if (node.name === '%context' || node.name === '%resource' || node.name === '%rootResource') {
|
|
908
|
+
return { type: 'Any', singleton: false }; // These return the original input
|
|
451
909
|
}
|
|
452
910
|
|
|
453
911
|
// User-defined variables - check with or without % prefix
|
|
@@ -464,13 +922,13 @@ export class Analyzer {
|
|
|
464
922
|
return { type: 'Any', singleton: true };
|
|
465
923
|
}
|
|
466
924
|
|
|
467
|
-
private inferCollectionType(node: CollectionNode): TypeInfo {
|
|
925
|
+
private async inferCollectionType(node: CollectionNode): Promise<TypeInfo> {
|
|
468
926
|
if (node.elements.length === 0) {
|
|
469
927
|
return { type: 'Any', singleton: false };
|
|
470
928
|
}
|
|
471
929
|
|
|
472
930
|
// Infer types of all elements
|
|
473
|
-
const elementTypes = node.elements.map(el => this.inferType(el));
|
|
931
|
+
const elementTypes = await Promise.all(node.elements.map(el => this.inferType(el)));
|
|
474
932
|
|
|
475
933
|
// If all elements have the same type, use that
|
|
476
934
|
const firstType = elementTypes[0];
|
|
@@ -490,12 +948,12 @@ export class Analyzer {
|
|
|
490
948
|
return { type: 'Any', singleton: false };
|
|
491
949
|
}
|
|
492
950
|
|
|
493
|
-
private inferTypeCastType(node: TypeCastNode): TypeInfo {
|
|
951
|
+
private async inferTypeCastType(node: TypeCastNode): Promise<TypeInfo> {
|
|
494
952
|
const targetType = node.targetType;
|
|
495
953
|
|
|
496
|
-
// If we have a model provider, try to get the type
|
|
954
|
+
// If we have a model provider, try to get the type
|
|
497
955
|
if (this.modelProvider) {
|
|
498
|
-
const typeInfo = this.modelProvider.getType(targetType);
|
|
956
|
+
const typeInfo = await this.modelProvider.getType(targetType);
|
|
499
957
|
if (typeInfo) {
|
|
500
958
|
return typeInfo;
|
|
501
959
|
}
|
|
@@ -626,17 +1084,14 @@ export class Analyzer {
|
|
|
626
1084
|
if (!foundMatch) {
|
|
627
1085
|
const leftTypeStr = this.typeToString(leftType);
|
|
628
1086
|
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
|
|
1087
|
+
this.diagnostics.push(
|
|
1088
|
+
toDiagnostic(Errors.operatorTypeMismatch(node.operator, leftTypeStr, rightTypeStr, node.range))
|
|
634
1089
|
);
|
|
635
1090
|
}
|
|
636
1091
|
}
|
|
637
1092
|
|
|
638
1093
|
private checkFunctionArgumentTypes(node: FunctionNode, func: import('./types').FunctionDefinition): void {
|
|
639
|
-
const params = func.
|
|
1094
|
+
const params = func.signatures?.[0]?.parameters || [];
|
|
640
1095
|
|
|
641
1096
|
for (let i = 0; i < Math.min(node.arguments.length, params.length); i++) {
|
|
642
1097
|
const arg = node.arguments[i]!;
|
|
@@ -647,11 +1102,8 @@ export class Analyzer {
|
|
|
647
1102
|
if (!this.isTypeCompatible(arg.typeInfo, param.type)) {
|
|
648
1103
|
const argTypeStr = this.typeToString(arg.typeInfo);
|
|
649
1104
|
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
|
|
1105
|
+
this.diagnostics.push(
|
|
1106
|
+
toDiagnostic(Errors.argumentTypeMismatch(i + 1, func.name, paramTypeStr, argTypeStr, arg.range))
|
|
655
1107
|
);
|
|
656
1108
|
}
|
|
657
1109
|
}
|
|
@@ -666,72 +1118,279 @@ export class Analyzer {
|
|
|
666
1118
|
return `${type.type}${singletonStr}`;
|
|
667
1119
|
}
|
|
668
1120
|
|
|
1121
|
+
/**
|
|
1122
|
+
* Infer the expected type at a cursor position based on context
|
|
1123
|
+
*/
|
|
1124
|
+
private inferExpectedTypeForCursor(cursorNode: AnyCursorNode, inputType?: TypeInfo): TypeInfo | undefined {
|
|
1125
|
+
const context = cursorNode.context;
|
|
1126
|
+
|
|
1127
|
+
switch (context) {
|
|
1128
|
+
case CursorContext.Identifier:
|
|
1129
|
+
// After dot, expecting a member of the input type
|
|
1130
|
+
return inputType;
|
|
1131
|
+
|
|
1132
|
+
case CursorContext.Type:
|
|
1133
|
+
// After is/as/ofType, expecting a type name
|
|
1134
|
+
return { type: 'System.String' as TypeName, singleton: true };
|
|
1135
|
+
|
|
1136
|
+
case CursorContext.Argument:
|
|
1137
|
+
// In function argument, would need to look up function signature
|
|
1138
|
+
// For now, return Any
|
|
1139
|
+
return { type: 'Any' as TypeName, singleton: false };
|
|
1140
|
+
|
|
1141
|
+
case CursorContext.Index:
|
|
1142
|
+
// In indexer, expecting Integer
|
|
1143
|
+
return { type: 'Integer' as TypeName, singleton: true };
|
|
1144
|
+
|
|
1145
|
+
case CursorContext.Operator:
|
|
1146
|
+
// Between expressions, could be any operator
|
|
1147
|
+
// Return the input type as context
|
|
1148
|
+
return inputType;
|
|
1149
|
+
|
|
1150
|
+
default:
|
|
1151
|
+
return undefined;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
669
1155
|
/**
|
|
670
1156
|
* Annotate AST with type information
|
|
671
1157
|
*/
|
|
672
|
-
private annotateAST(node: ASTNode, inputType?: TypeInfo): void {
|
|
1158
|
+
private async annotateAST(node: ASTNode, inputType?: TypeInfo): Promise<void> {
|
|
1159
|
+
// Check for cursor node in cursor mode
|
|
1160
|
+
if (this.cursorMode && isCursorNode(node)) {
|
|
1161
|
+
this.stoppedAtCursor = true;
|
|
1162
|
+
this.cursorContext = {
|
|
1163
|
+
cursorNode: node as AnyCursorNode,
|
|
1164
|
+
typeBeforeCursor: inputType,
|
|
1165
|
+
expectedType: this.inferExpectedTypeForCursor(node as AnyCursorNode, inputType)
|
|
1166
|
+
};
|
|
1167
|
+
// Still attach a type to the cursor node for consistency
|
|
1168
|
+
(node as any).typeInfo = inputType || { type: 'Any', singleton: false };
|
|
1169
|
+
return; // Short-circuit
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// If we've already stopped at cursor, don't continue
|
|
1173
|
+
if (this.stoppedAtCursor) {
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
673
1177
|
// Handle error nodes
|
|
674
1178
|
if (node.type === 'Error') {
|
|
675
1179
|
const errorNode = node as ErrorNode;
|
|
676
1180
|
// Infer a reasonable type for error nodes
|
|
677
1181
|
node.typeInfo = this.inferErrorNodeType(errorNode, inputType);
|
|
678
1182
|
// Add diagnostic for the error
|
|
679
|
-
this.
|
|
680
|
-
errorNode.severity || DiagnosticSeverity.Error,
|
|
681
|
-
errorNode.message,
|
|
682
|
-
errorNode,
|
|
683
|
-
errorNode.code?.toString() ||
|
|
684
|
-
|
|
1183
|
+
this.diagnostics.push({
|
|
1184
|
+
severity: errorNode.severity || DiagnosticSeverity.Error,
|
|
1185
|
+
message: errorNode.message,
|
|
1186
|
+
range: errorNode.range,
|
|
1187
|
+
code: errorNode.code?.toString() || 'FP5003',
|
|
1188
|
+
source: 'fhirpath'
|
|
1189
|
+
});
|
|
685
1190
|
return;
|
|
686
1191
|
}
|
|
687
1192
|
|
|
688
1193
|
// Infer and attach type info
|
|
689
|
-
node.typeInfo = this.inferType(node, inputType);
|
|
1194
|
+
node.typeInfo = await this.inferType(node, inputType);
|
|
690
1195
|
|
|
691
1196
|
// Recursively annotate children
|
|
692
1197
|
switch (node.type) {
|
|
693
1198
|
case NodeType.Binary:
|
|
694
1199
|
const binaryNode = node as BinaryNode;
|
|
695
|
-
this.annotateAST(binaryNode.left, inputType);
|
|
1200
|
+
await this.annotateAST(binaryNode.left, inputType);
|
|
1201
|
+
|
|
1202
|
+
// If we stopped at cursor, don't continue
|
|
1203
|
+
if (this.stoppedAtCursor) {
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// Check if right side is a cursor node - if so, set type from left
|
|
1208
|
+
if (this.cursorMode && isCursorNode(binaryNode.right)) {
|
|
1209
|
+
this.stoppedAtCursor = true;
|
|
1210
|
+
this.cursorContext = {
|
|
1211
|
+
cursorNode: binaryNode.right as AnyCursorNode,
|
|
1212
|
+
typeBeforeCursor: binaryNode.left.typeInfo,
|
|
1213
|
+
expectedType: this.inferExpectedTypeForCursor(binaryNode.right as AnyCursorNode, binaryNode.left.typeInfo)
|
|
1214
|
+
};
|
|
1215
|
+
// Still attach type to cursor node
|
|
1216
|
+
(binaryNode.right as any).typeInfo = binaryNode.left.typeInfo || { type: 'Any', singleton: false };
|
|
1217
|
+
break;
|
|
1218
|
+
}
|
|
696
1219
|
|
|
697
1220
|
// For navigation, pass the left's type as input to the right
|
|
698
1221
|
if (binaryNode.operator === '.') {
|
|
699
|
-
|
|
1222
|
+
// Collect all variables defined in the left side chain
|
|
1223
|
+
const definedVarsWithTypes = this.collectDefinedVariablesWithTypes(binaryNode.left);
|
|
1224
|
+
|
|
1225
|
+
if (definedVarsWithTypes.size > 0) {
|
|
1226
|
+
// Save current variable types
|
|
1227
|
+
const savedTypes = new Map<string, TypeInfo>();
|
|
1228
|
+
definedVarsWithTypes.forEach((type, varName) => {
|
|
1229
|
+
const currentType = this.userVariableTypes.get(varName);
|
|
1230
|
+
if (currentType) {
|
|
1231
|
+
savedTypes.set(varName, currentType);
|
|
1232
|
+
}
|
|
1233
|
+
this.userVariableTypes.set(varName, type);
|
|
1234
|
+
});
|
|
1235
|
+
|
|
1236
|
+
// Annotate right side with new variables in scope
|
|
1237
|
+
await this.annotateAST(binaryNode.right, binaryNode.left.typeInfo);
|
|
1238
|
+
|
|
1239
|
+
// Restore previous types
|
|
1240
|
+
definedVarsWithTypes.forEach((_, varName) => {
|
|
1241
|
+
const savedType = savedTypes.get(varName);
|
|
1242
|
+
if (savedType) {
|
|
1243
|
+
this.userVariableTypes.set(varName, savedType);
|
|
1244
|
+
} else {
|
|
1245
|
+
this.userVariableTypes.delete(varName);
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
} else {
|
|
1249
|
+
// No defineVariable in chain, proceed normally
|
|
1250
|
+
await this.annotateAST(binaryNode.right, binaryNode.left.typeInfo);
|
|
1251
|
+
}
|
|
700
1252
|
} else {
|
|
701
|
-
this.annotateAST(binaryNode.right, inputType);
|
|
1253
|
+
await this.annotateAST(binaryNode.right, inputType);
|
|
702
1254
|
}
|
|
703
1255
|
break;
|
|
704
1256
|
|
|
705
1257
|
case NodeType.Unary:
|
|
706
1258
|
const unaryNode = node as UnaryNode;
|
|
707
|
-
this.annotateAST(unaryNode.operand, inputType);
|
|
1259
|
+
await this.annotateAST(unaryNode.operand, inputType);
|
|
708
1260
|
break;
|
|
709
1261
|
|
|
710
1262
|
case NodeType.Function:
|
|
711
1263
|
const funcNode = node as FunctionNode;
|
|
712
|
-
this.annotateAST(funcNode.name, inputType);
|
|
713
|
-
|
|
1264
|
+
await this.annotateAST(funcNode.name, inputType);
|
|
1265
|
+
|
|
1266
|
+
// Special handling for aggregate function arguments
|
|
1267
|
+
if (funcNode.name.type === NodeType.Identifier &&
|
|
1268
|
+
(funcNode.name as IdentifierNode).name === 'aggregate') {
|
|
1269
|
+
// Aggregate establishes both $this and $total
|
|
1270
|
+
if (funcNode.arguments.length >= 1) {
|
|
1271
|
+
const itemType = inputType ? { ...inputType, singleton: true } : { type: 'Any' as TypeName, singleton: true };
|
|
1272
|
+
|
|
1273
|
+
// Save current system variable context
|
|
1274
|
+
const savedThis = this.systemVariableTypes.get('$this');
|
|
1275
|
+
const savedTotal = this.systemVariableTypes.get('$total');
|
|
1276
|
+
|
|
1277
|
+
// Set $this for iteration
|
|
1278
|
+
this.systemVariableTypes.set('$this', itemType);
|
|
1279
|
+
|
|
1280
|
+
if (funcNode.arguments.length >= 2) {
|
|
1281
|
+
// Has init parameter - evaluate it first
|
|
1282
|
+
await this.annotateAST(funcNode.arguments[1]!, inputType);
|
|
1283
|
+
const initType = funcNode.arguments[1]!.typeInfo;
|
|
1284
|
+
|
|
1285
|
+
// Set $total to init type
|
|
1286
|
+
if (initType) {
|
|
1287
|
+
this.systemVariableTypes.set('$total', initType);
|
|
1288
|
+
} else {
|
|
1289
|
+
this.systemVariableTypes.set('$total', { type: 'Any', singleton: false });
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// Process aggregator with both variables set
|
|
1293
|
+
await this.annotateAST(funcNode.arguments[0]!, inputType);
|
|
1294
|
+
|
|
1295
|
+
// Process remaining arguments
|
|
1296
|
+
for (const arg of funcNode.arguments.slice(2)) {
|
|
1297
|
+
await this.annotateAST(arg, inputType);
|
|
1298
|
+
if (this.stoppedAtCursor) break;
|
|
1299
|
+
}
|
|
1300
|
+
} else {
|
|
1301
|
+
// No init - first pass to infer aggregator type
|
|
1302
|
+
this.systemVariableTypes.set('$total', { type: 'Any', singleton: false });
|
|
1303
|
+
await this.annotateAST(funcNode.arguments[0]!, inputType);
|
|
1304
|
+
|
|
1305
|
+
// Second pass with inferred type
|
|
1306
|
+
const aggregatorType = funcNode.arguments[0]!.typeInfo;
|
|
1307
|
+
if (aggregatorType) {
|
|
1308
|
+
this.systemVariableTypes.set('$total', aggregatorType);
|
|
1309
|
+
// Re-annotate with proper $total type
|
|
1310
|
+
await this.annotateAST(funcNode.arguments[0]!, inputType);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Restore previous context
|
|
1315
|
+
if (savedThis) {
|
|
1316
|
+
this.systemVariableTypes.set('$this', savedThis);
|
|
1317
|
+
} else {
|
|
1318
|
+
this.systemVariableTypes.delete('$this');
|
|
1319
|
+
}
|
|
1320
|
+
if (savedTotal) {
|
|
1321
|
+
this.systemVariableTypes.set('$total', savedTotal);
|
|
1322
|
+
} else {
|
|
1323
|
+
this.systemVariableTypes.delete('$total');
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
} else {
|
|
1327
|
+
// Special handling for functions that pass their input as context to arguments
|
|
1328
|
+
const funcName = funcNode.name.type === NodeType.Identifier ?
|
|
1329
|
+
(funcNode.name as IdentifierNode).name : null;
|
|
1330
|
+
|
|
1331
|
+
if (funcName && ['where', 'select', 'all', 'exists'].includes(funcName)) {
|
|
1332
|
+
// These functions establish $this as each element of the input collection
|
|
1333
|
+
const elementType = inputType ? { ...inputType, singleton: true } : { type: 'Any' as TypeName, singleton: true };
|
|
1334
|
+
|
|
1335
|
+
// Save current system variable context
|
|
1336
|
+
const savedThis = this.systemVariableTypes.get('$this');
|
|
1337
|
+
const savedIndex = this.systemVariableTypes.get('$index');
|
|
1338
|
+
|
|
1339
|
+
// Set system variables for expression evaluation
|
|
1340
|
+
this.systemVariableTypes.set('$this', elementType);
|
|
1341
|
+
this.systemVariableTypes.set('$index', { type: 'Integer', singleton: true });
|
|
1342
|
+
|
|
1343
|
+
// Process arguments with system variables in scope
|
|
1344
|
+
for (const arg of funcNode.arguments) {
|
|
1345
|
+
await this.annotateAST(arg, inputType);
|
|
1346
|
+
if (this.stoppedAtCursor) break;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Restore previous context
|
|
1350
|
+
if (savedThis) {
|
|
1351
|
+
this.systemVariableTypes.set('$this', savedThis);
|
|
1352
|
+
} else {
|
|
1353
|
+
this.systemVariableTypes.delete('$this');
|
|
1354
|
+
}
|
|
1355
|
+
if (savedIndex) {
|
|
1356
|
+
this.systemVariableTypes.set('$index', savedIndex);
|
|
1357
|
+
} else {
|
|
1358
|
+
this.systemVariableTypes.delete('$index');
|
|
1359
|
+
}
|
|
1360
|
+
} else {
|
|
1361
|
+
// Regular function argument annotation
|
|
1362
|
+
for (const arg of funcNode.arguments) {
|
|
1363
|
+
await this.annotateAST(arg, inputType);
|
|
1364
|
+
if (this.stoppedAtCursor) break;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
714
1368
|
break;
|
|
715
1369
|
|
|
716
1370
|
case NodeType.Collection:
|
|
717
1371
|
const collNode = node as CollectionNode;
|
|
718
|
-
|
|
1372
|
+
for (const el of collNode.elements) {
|
|
1373
|
+
await this.annotateAST(el, inputType);
|
|
1374
|
+
if (this.stoppedAtCursor) break;
|
|
1375
|
+
}
|
|
719
1376
|
break;
|
|
720
1377
|
|
|
721
1378
|
case NodeType.TypeCast:
|
|
722
1379
|
const castNode = node as TypeCastNode;
|
|
723
|
-
this.annotateAST(castNode.expression, inputType);
|
|
1380
|
+
await this.annotateAST(castNode.expression, inputType);
|
|
724
1381
|
break;
|
|
725
1382
|
|
|
726
1383
|
case NodeType.MembershipTest:
|
|
727
1384
|
const memberNode = node as MembershipTestNode;
|
|
728
|
-
this.annotateAST(memberNode.expression, inputType);
|
|
1385
|
+
await this.annotateAST(memberNode.expression, inputType);
|
|
729
1386
|
break;
|
|
730
1387
|
|
|
731
1388
|
case NodeType.Index:
|
|
732
1389
|
const indexNode = node as IndexNode;
|
|
733
|
-
this.annotateAST(indexNode.expression, inputType);
|
|
734
|
-
this.
|
|
1390
|
+
await this.annotateAST(indexNode.expression, inputType);
|
|
1391
|
+
if (!this.stoppedAtCursor) {
|
|
1392
|
+
await this.annotateAST(indexNode.index, inputType);
|
|
1393
|
+
}
|
|
735
1394
|
break;
|
|
736
1395
|
|
|
737
1396
|
case NodeType.TypeOrIdentifier:
|