@atomic-ehr/fhirpath 0.0.2 → 0.0.3-canary.2be66fb.20250905161900
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 +226 -120
- package/dist/index.js +11552 -5580
- package/dist/index.js.map +1 -1
- package/package.json +12 -5
- 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 +939 -1204
- package/src/completion-provider.ts +209 -191
- package/src/complex-types/quantity-value.ts +410 -0
- package/src/complex-types/temporal.ts +1776 -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 +506 -468
- package/src/lexer.ts +192 -211
- 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 +99 -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 +744 -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 +132 -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/extension-function.ts +84 -0
- package/src/operations/first-function.ts +1 -1
- package/src/operations/floor-function.ts +1 -1
- package/src/operations/greater-operator.ts +7 -9
- package/src/operations/greater-or-equal-operator.ts +7 -9
- package/src/operations/highBoundary-function.ts +120 -0
- package/src/operations/hourOf-function.ts +66 -0
- package/src/operations/iif-function.ts +193 -8
- package/src/operations/implies-operator.ts +2 -1
- package/src/operations/in-operator.ts +2 -1
- package/src/operations/index.ts +43 -0
- package/src/operations/indexOf-function.ts +1 -1
- package/src/operations/intersect-function.ts +1 -1
- package/src/operations/is-function.ts +70 -0
- package/src/operations/is-operator.ts +176 -13
- 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 +8 -9
- package/src/operations/less-or-equal-operator.ts +7 -9
- package/src/operations/less-than.ts +8 -13
- 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 +76 -4
- package/src/operations/minuteOf-function.ts +66 -0
- package/src/operations/mod-operator.ts +8 -2
- 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 +10 -3
- package/src/operations/ofType-function.ts +43 -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/precision-function.ts +146 -0
- package/src/operations/repeat-function.ts +169 -0
- package/src/operations/replace-function.ts +1 -1
- package/src/operations/replaceMatches-function.ts +125 -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 +78 -15
- 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 +262 -503
- package/src/registry.ts +53 -42
- package/src/types.ts +129 -17
- package/src/utils/decimal.ts +76 -0
- package/src/utils/pprint.ts +151 -0
- package/src/quantity-value.ts +0 -198
package/src/analyzer.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
BinaryNode,
|
|
4
4
|
IdentifierNode,
|
|
5
5
|
LiteralNode,
|
|
6
|
+
TemporalLiteralNode,
|
|
6
7
|
FunctionNode,
|
|
7
8
|
Diagnostic,
|
|
8
9
|
AnalysisResult,
|
|
@@ -15,15 +16,20 @@ import type {
|
|
|
15
16
|
ModelProvider,
|
|
16
17
|
VariableNode,
|
|
17
18
|
TypeName,
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
ErrorNode,
|
|
20
|
+
InternalAnalysisResult,
|
|
21
|
+
QuantityNode
|
|
20
22
|
} from './types';
|
|
21
|
-
import { NodeType, DiagnosticSeverity } from './types';
|
|
23
|
+
import { NodeType, DiagnosticSeverity, AnalysisContext } from './types';
|
|
24
|
+
import type { OperatorSignature, FunctionSignature } from './types';
|
|
25
|
+
import type { FunctionDefinition } from './types';
|
|
22
26
|
import { registry } from './registry';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import
|
|
26
|
-
|
|
27
|
+
import { matchOperatorSignature, matchFunctionSignature, resolveResultType } from './analyzer/type-compat';
|
|
28
|
+
import { checkParamTypes, formatType, isEmptyCollection, isUnionType, getUnionChoices, validateUnionChoice } from './analyzer/utils';
|
|
29
|
+
import { Errors, toDiagnostic, ErrorCodes } from './errors';
|
|
30
|
+
import { isCursorNode, CursorContext } from './parser/cursor-nodes';
|
|
31
|
+
import type { AnyCursorNode } from './parser/cursor-nodes';
|
|
32
|
+
import { Parser, type ParserOptions } from './parser';
|
|
27
33
|
|
|
28
34
|
export interface AnalyzerOptions {
|
|
29
35
|
cursorMode?: boolean;
|
|
@@ -35,992 +41,1023 @@ export interface AnalysisResultWithCursor extends AnalysisResult {
|
|
|
35
41
|
typeBeforeCursor?: TypeInfo;
|
|
36
42
|
expectedType?: TypeInfo;
|
|
37
43
|
cursorNode?: AnyCursorNode;
|
|
44
|
+
functionCall?: {
|
|
45
|
+
definition: import('./types').FunctionDefinition;
|
|
46
|
+
argumentIndex: number;
|
|
47
|
+
};
|
|
38
48
|
};
|
|
39
49
|
}
|
|
40
50
|
|
|
41
51
|
export class Analyzer {
|
|
42
|
-
private diagnostics: Diagnostic[] = [];
|
|
43
|
-
private variables: Set<string> = new Set(['$this', '$index', '$total', 'context', 'resource', 'rootResource']);
|
|
44
52
|
private modelProvider?: ModelProvider;
|
|
45
|
-
private userVariableTypes: Map<string, TypeInfo> = new Map();
|
|
46
|
-
private systemVariableTypes: Map<string, TypeInfo> = new Map();
|
|
47
53
|
private cursorMode: boolean = false;
|
|
48
54
|
private stoppedAtCursor: boolean = false;
|
|
49
55
|
private cursorContext?: {
|
|
50
56
|
typeBeforeCursor?: TypeInfo;
|
|
51
57
|
expectedType?: TypeInfo;
|
|
52
58
|
cursorNode?: AnyCursorNode;
|
|
59
|
+
functionCall?: {
|
|
60
|
+
definition: import('./types').FunctionDefinition;
|
|
61
|
+
argumentIndex: number;
|
|
62
|
+
};
|
|
53
63
|
};
|
|
54
64
|
|
|
55
65
|
constructor(modelProvider?: ModelProvider) {
|
|
56
66
|
this.modelProvider = modelProvider;
|
|
57
67
|
}
|
|
58
68
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Parse and analyze a FHIRPath expression end-to-end, returning an AnalysisResult
|
|
71
|
+
* and structured diagnostics. Supports optional error recovery (LSP mode).
|
|
72
|
+
*/
|
|
73
|
+
static async analyzeExpression(
|
|
74
|
+
expression: string,
|
|
75
|
+
options: {
|
|
76
|
+
variables?: Record<string, unknown>;
|
|
77
|
+
modelProvider?: ModelProvider;
|
|
78
|
+
inputType?: TypeInfo;
|
|
79
|
+
errorRecovery?: boolean;
|
|
80
|
+
parserOptions?: ParserOptions;
|
|
81
|
+
} = {}
|
|
64
82
|
): Promise<AnalysisResultWithCursor> {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (value !== undefined && value !== null) {
|
|
77
|
-
this.userVariableTypes.set(name, this.inferValueType(value));
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Annotate AST with type information
|
|
83
|
-
await this.annotateAST(ast, inputType);
|
|
84
|
-
|
|
85
|
-
// Perform validation with type checking (if not stopped at cursor)
|
|
86
|
-
if (!this.stoppedAtCursor) {
|
|
87
|
-
this.visitNode(ast);
|
|
83
|
+
const parserOptions: ParserOptions | undefined = options.errorRecovery
|
|
84
|
+
? { mode: 'lsp', errorRecovery: true, ...(options.parserOptions || {}) }
|
|
85
|
+
: options.parserOptions;
|
|
86
|
+
|
|
87
|
+
const parser = new Parser(expression, parserOptions);
|
|
88
|
+
const parseResult = parser.parse();
|
|
89
|
+
|
|
90
|
+
// If error recovery is not enabled and there are parse errors, surface the first one.
|
|
91
|
+
if (!options.errorRecovery && parseResult.errors.length > 0) {
|
|
92
|
+
// Preserve backward compatibility; analyzer doesn't throw custom mapping here.
|
|
93
|
+
throw Errors.invalidSyntax(parseResult.errors[0]!.message);
|
|
88
94
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
ast,
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
|
|
96
|
+
const analyzer = new Analyzer(options.modelProvider);
|
|
97
|
+
const result = await analyzer.analyze(
|
|
98
|
+
parseResult.ast,
|
|
99
|
+
options.variables,
|
|
100
|
+
options.inputType,
|
|
101
|
+
{ cursorMode: !!parserOptions?.mode && parserOptions.mode === 'lsp' }
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return result;
|
|
96
105
|
}
|
|
97
106
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return; // Short-circuit
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Handle error nodes - process them for diagnostics but don't traverse
|
|
110
|
-
if (node.type === 'Error') {
|
|
111
|
-
// Diagnostics already added in annotateAST
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// If we've already stopped at cursor, don't continue
|
|
116
|
-
if (this.stoppedAtCursor) {
|
|
117
|
-
return;
|
|
107
|
+
/**
|
|
108
|
+
* Main entry point for context-flow analysis.
|
|
109
|
+
* Analyzes a node with the given context.
|
|
110
|
+
*/
|
|
111
|
+
private async analyzeNode(node: ASTNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
|
|
112
|
+
// Handle cursor nodes for completion
|
|
113
|
+
if (isCursorNode(node)) {
|
|
114
|
+
return this.analyzeCursorNode(node as AnyCursorNode, context);
|
|
118
115
|
}
|
|
116
|
+
|
|
117
|
+
let result: InternalAnalysisResult;
|
|
119
118
|
|
|
120
119
|
switch (node.type) {
|
|
121
120
|
case NodeType.Binary:
|
|
122
|
-
this.
|
|
121
|
+
result = await this.analyzeBinary(node as BinaryNode, context);
|
|
123
122
|
break;
|
|
124
|
-
case NodeType.
|
|
125
|
-
this.
|
|
123
|
+
case NodeType.Unary:
|
|
124
|
+
result = await this.analyzeUnary(node as UnaryNode, context);
|
|
126
125
|
break;
|
|
127
126
|
case NodeType.Function:
|
|
128
|
-
this.
|
|
127
|
+
result = await this.analyzeFunction(node as FunctionNode, context);
|
|
128
|
+
break;
|
|
129
|
+
case NodeType.Variable:
|
|
130
|
+
result = this.analyzeVariable(node as VariableNode, context);
|
|
131
|
+
break;
|
|
132
|
+
case NodeType.Identifier:
|
|
133
|
+
result = await this.analyzeIdentifier(node as IdentifierNode, context);
|
|
134
|
+
break;
|
|
135
|
+
case NodeType.Literal:
|
|
136
|
+
result = this.analyzeLiteral(node as LiteralNode, context);
|
|
137
|
+
break;
|
|
138
|
+
case NodeType.TemporalLiteral:
|
|
139
|
+
result = this.analyzeTemporalLiteral(node as TemporalLiteralNode, context);
|
|
129
140
|
break;
|
|
130
141
|
case NodeType.Index:
|
|
131
|
-
|
|
132
|
-
this.visitNode(indexNode.expression);
|
|
133
|
-
this.visitNode(indexNode.index);
|
|
142
|
+
result = await this.analyzeIndex(node as IndexNode, context);
|
|
134
143
|
break;
|
|
135
144
|
case NodeType.Collection:
|
|
136
|
-
(node as CollectionNode
|
|
137
|
-
break;
|
|
138
|
-
case NodeType.Unary:
|
|
139
|
-
this.visitNode((node as UnaryNode).operand);
|
|
145
|
+
result = await this.analyzeCollection(node as CollectionNode, context);
|
|
140
146
|
break;
|
|
141
147
|
case NodeType.MembershipTest:
|
|
142
|
-
this.
|
|
148
|
+
result = await this.analyzeMembershipTest(node as MembershipTestNode, context);
|
|
143
149
|
break;
|
|
144
150
|
case NodeType.TypeCast:
|
|
145
|
-
this.
|
|
151
|
+
result = await this.analyzeTypeCast(node as TypeCastNode, context);
|
|
146
152
|
break;
|
|
147
|
-
case NodeType.
|
|
148
|
-
this.
|
|
153
|
+
case NodeType.Quantity:
|
|
154
|
+
result = this.analyzeQuantity(node as QuantityNode, context);
|
|
149
155
|
break;
|
|
150
|
-
case
|
|
151
|
-
|
|
152
|
-
case NodeType.TypeReference:
|
|
153
|
-
// These are always valid
|
|
156
|
+
case 'Error':
|
|
157
|
+
result = this.analyzeError(node as ErrorNode, context);
|
|
154
158
|
break;
|
|
159
|
+
default:
|
|
160
|
+
result = {
|
|
161
|
+
type: { type: 'Any', singleton: false },
|
|
162
|
+
diagnostics: [toDiagnostic(Errors.unknownNodeType(String((node as any)?.type), (node as any)?.range))]
|
|
163
|
+
};
|
|
155
164
|
}
|
|
165
|
+
|
|
166
|
+
// Annotate the node with type information
|
|
167
|
+
node.typeInfo = result.type;
|
|
168
|
+
|
|
169
|
+
return result;
|
|
156
170
|
}
|
|
157
171
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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;
|
|
172
|
+
/**
|
|
173
|
+
* Analyzes binary operators with special handling for union and dot.
|
|
174
|
+
*/
|
|
175
|
+
private async analyzeBinary(node: BinaryNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
|
|
176
|
+
const diagnostics: Diagnostic[] = [];
|
|
177
|
+
|
|
178
|
+
// Special handling for union operator - fork context for each branch
|
|
179
|
+
if (node.operator === '|') {
|
|
180
|
+
const leftResult = await this.analyzeNode(node.left, context.fork());
|
|
181
|
+
if (this.stoppedAtCursor) {
|
|
182
|
+
return { type: { type: 'Any', singleton: false }, diagnostics: leftResult.diagnostics };
|
|
184
183
|
}
|
|
184
|
+
|
|
185
|
+
const rightResult = await this.analyzeNode(node.right, context.fork());
|
|
186
|
+
|
|
187
|
+
diagnostics.push(...leftResult.diagnostics, ...rightResult.diagnostics);
|
|
188
|
+
|
|
189
|
+
// Preserve left operand type per operator signature (leftType)
|
|
190
|
+
const type = { ...leftResult.type, singleton: false };
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
type,
|
|
194
|
+
diagnostics,
|
|
195
|
+
context // Return original context unchanged - no variable leakage
|
|
196
|
+
};
|
|
185
197
|
}
|
|
186
|
-
|
|
187
|
-
// Special handling for dot operator
|
|
188
|
-
if (node.operator === '.'
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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) {
|
|
213
|
-
const inputTypeStr = this.typeToString(node.left.typeInfo);
|
|
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)
|
|
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
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
198
|
+
|
|
199
|
+
// Special handling for dot operator - flow context through
|
|
200
|
+
if (node.operator === '.') {
|
|
201
|
+
// Check if this is actually a namespaced type in an 'is' expression
|
|
202
|
+
// Parser incorrectly creates: (true is System).Boolean instead of: true is System.Boolean
|
|
203
|
+
if (node.left.type === NodeType.MembershipTest && node.right.type === NodeType.Identifier) {
|
|
204
|
+
const membershipTest = node.left as MembershipTestNode;
|
|
205
|
+
const rightIdent = node.right as IdentifierNode;
|
|
206
|
+
// Reconstruct the correct MembershipTest with full type name
|
|
207
|
+
const correctedNode: MembershipTestNode = {
|
|
208
|
+
...membershipTest,
|
|
209
|
+
targetType: `${membershipTest.targetType}.${rightIdent.name}`
|
|
210
|
+
};
|
|
211
|
+
return await this.analyzeMembershipTest(correctedNode, context);
|
|
241
212
|
}
|
|
213
|
+
|
|
214
|
+
const leftResult = await this.analyzeNode(node.left, context);
|
|
215
|
+
if (this.stoppedAtCursor) {
|
|
216
|
+
return { type: { type: 'Any', singleton: false }, diagnostics: leftResult.diagnostics };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Right side gets left's output as input, with any context changes
|
|
220
|
+
const rightContext = (leftResult.context || context).withInputType(leftResult.type);
|
|
221
|
+
const rightResult = await this.analyzeNode(node.right, rightContext);
|
|
222
|
+
|
|
223
|
+
diagnostics.push(...leftResult.diagnostics, ...rightResult.diagnostics);
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
type: rightResult.type,
|
|
227
|
+
diagnostics,
|
|
228
|
+
context: rightResult.context // Propagate context changes (for defineVariable)
|
|
229
|
+
};
|
|
242
230
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return;
|
|
231
|
+
|
|
232
|
+
// Handle other binary operators
|
|
233
|
+
const leftResult = await this.analyzeNode(node.left, context);
|
|
234
|
+
if (this.stoppedAtCursor) {
|
|
235
|
+
return { type: { type: 'Any', singleton: false }, diagnostics: leftResult.diagnostics };
|
|
249
236
|
}
|
|
250
237
|
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
this.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
238
|
+
// Check if right side is a cursor node - if so, set proper context
|
|
239
|
+
if (this.cursorMode && isCursorNode(node.right)) {
|
|
240
|
+
this.stoppedAtCursor = true;
|
|
241
|
+
this.cursorContext = {
|
|
242
|
+
cursorNode: node.right as AnyCursorNode,
|
|
243
|
+
typeBeforeCursor: leftResult.type,
|
|
244
|
+
expectedType: undefined
|
|
245
|
+
};
|
|
246
|
+
return {
|
|
247
|
+
type: { type: 'Any', singleton: false },
|
|
248
|
+
diagnostics: leftResult.diagnostics
|
|
249
|
+
};
|
|
257
250
|
}
|
|
258
251
|
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
252
|
+
// For most operators, right side evaluates with original context (not left's output)
|
|
253
|
+
const rightResult = await this.analyzeNode(node.right, context);
|
|
254
|
+
|
|
255
|
+
diagnostics.push(...leftResult.diagnostics, ...rightResult.diagnostics);
|
|
256
|
+
|
|
257
|
+
// Get operator definition for type checking
|
|
258
|
+
const operatorDef = registry.getOperatorDefinition(node.operator);
|
|
259
|
+
if (!operatorDef) {
|
|
260
|
+
diagnostics.push(toDiagnostic(Errors.unknownOperator(node.operator, node.range)));
|
|
261
|
+
return {
|
|
262
|
+
type: { type: 'Any', singleton: false },
|
|
263
|
+
diagnostics
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check operator signatures for type compatibility
|
|
268
|
+
if (operatorDef.signatures && operatorDef.signatures.length > 0) {
|
|
269
|
+
const matchingSignature = matchOperatorSignature(leftResult.type, rightResult.type, operatorDef) || null;
|
|
270
|
+
if (!matchingSignature) {
|
|
271
|
+
// No matching signature found - report type error
|
|
272
|
+
// But don't report if either side is Any (could be from an error)
|
|
273
|
+
if (leftResult.type.type !== 'Any' && rightResult.type.type !== 'Any') {
|
|
274
|
+
const leftTypeStr = leftResult.type.singleton ? leftResult.type.type : `${leftResult.type.type}[]`;
|
|
275
|
+
const rightTypeStr = rightResult.type.singleton ? rightResult.type.type : `${rightResult.type.type}[]`;
|
|
276
|
+
diagnostics.push(this.createError(
|
|
277
|
+
node,
|
|
278
|
+
`Operator '${node.operator}' cannot be applied to types ${leftTypeStr} and ${rightTypeStr}`,
|
|
279
|
+
ErrorCodes.OPERATOR_TYPE_MISMATCH
|
|
280
|
+
));
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
type: { type: 'Any', singleton: false },
|
|
284
|
+
diagnostics
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Determine result type from matching signature
|
|
289
|
+
const resultType = resolveResultType(matchingSignature.result as any, {
|
|
290
|
+
input: context.inputType,
|
|
291
|
+
left: leftResult.type,
|
|
292
|
+
right: rightResult.type,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return {
|
|
296
|
+
type: resultType,
|
|
297
|
+
diagnostics
|
|
298
|
+
};
|
|
262
299
|
}
|
|
300
|
+
|
|
301
|
+
// If no signatures defined, return Any type
|
|
302
|
+
return {
|
|
303
|
+
type: { type: 'Any', singleton: false },
|
|
304
|
+
diagnostics
|
|
305
|
+
};
|
|
263
306
|
}
|
|
264
307
|
|
|
265
|
-
|
|
266
|
-
|
|
308
|
+
/**
|
|
309
|
+
* Analyzes function calls, delegating to function's analyze method if available.
|
|
310
|
+
*/
|
|
311
|
+
private async analyzeFunction(node: FunctionNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
|
|
312
|
+
const diagnostics: Diagnostic[] = [];
|
|
313
|
+
|
|
314
|
+
const functionName = this.getFunctionName(node);
|
|
315
|
+
if (!functionName) {
|
|
316
|
+
diagnostics.push(this.createError(node.name, 'Invalid function name', ErrorCodes.INVALID_SYNTAX));
|
|
317
|
+
return { type: { type: 'Any', singleton: false }, diagnostics };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const funcDef = registry.getFunction(functionName);
|
|
321
|
+
if (!funcDef) {
|
|
322
|
+
diagnostics.push(toDiagnostic(Errors.unknownFunction(functionName, node.range)));
|
|
323
|
+
return { type: { type: 'Any', singleton: false }, diagnostics };
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const arity = this.validateArity(funcDef, node, functionName);
|
|
327
|
+
diagnostics.push(...arity.diagnostics);
|
|
328
|
+
|
|
329
|
+
// Early union rules for ofType/is/as
|
|
330
|
+
diagnostics.push(...this.validateUnionTypeFilters(functionName, node, context));
|
|
331
|
+
|
|
332
|
+
// Custom analyze
|
|
333
|
+
if (funcDef.analyze) {
|
|
334
|
+
const result = funcDef.analyze(context, node.arguments);
|
|
335
|
+
const analysisResult = result instanceof Promise ? await result : result;
|
|
336
|
+
return {
|
|
337
|
+
...analysisResult,
|
|
338
|
+
diagnostics: [...diagnostics, ...analysisResult.diagnostics]
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Default path: analyze args
|
|
343
|
+
const argAnalysis = await this.analyzeArguments(funcDef, node, context, functionName);
|
|
344
|
+
diagnostics.push(...argAnalysis.diagnostics);
|
|
345
|
+
if (this.stoppedAtCursor) {
|
|
346
|
+
return { type: { type: 'Any', singleton: false }, diagnostics };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Signature matching and diagnostics
|
|
350
|
+
const signatureResult = this.matchAndDiagnoseSignature(
|
|
351
|
+
funcDef,
|
|
352
|
+
context.inputType,
|
|
353
|
+
argAnalysis.argTypes,
|
|
354
|
+
node,
|
|
355
|
+
functionName,
|
|
356
|
+
arity.hasError
|
|
357
|
+
);
|
|
358
|
+
diagnostics.push(...signatureResult.diagnostics);
|
|
359
|
+
if (signatureResult.earlyReturn) {
|
|
360
|
+
return { type: signatureResult.earlyReturn, diagnostics };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Empty propagation
|
|
364
|
+
if (this.propagatesEmpty(funcDef, context.inputType, argAnalysis.argTypes)) {
|
|
365
|
+
return {
|
|
366
|
+
type: { type: 'Any', singleton: false, isEmpty: true },
|
|
367
|
+
diagnostics,
|
|
368
|
+
context
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Result inference
|
|
373
|
+
let resultType = await this.inferFunctionResultType(
|
|
374
|
+
funcDef,
|
|
375
|
+
node,
|
|
376
|
+
context,
|
|
377
|
+
argAnalysis.argTypes,
|
|
378
|
+
signatureResult.match
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
if (functionName === 'where') {
|
|
382
|
+
resultType = { ...resultType, singleton: false };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { type: resultType, diagnostics, context };
|
|
267
386
|
}
|
|
268
387
|
|
|
269
|
-
private
|
|
388
|
+
private getFunctionName(node: FunctionNode): string | null {
|
|
270
389
|
if (node.name.type === NodeType.Identifier) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
390
|
+
return (node.name as IdentifierNode).name;
|
|
391
|
+
}
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private validateArity(
|
|
396
|
+
funcDef: FunctionDefinition,
|
|
397
|
+
node: FunctionNode,
|
|
398
|
+
functionName: string
|
|
399
|
+
): { diagnostics: Diagnostic[]; hasError: boolean } {
|
|
400
|
+
const diagnostics: Diagnostic[] = [];
|
|
401
|
+
let hasError = false;
|
|
402
|
+
if (funcDef.signatures && funcDef.signatures.length > 0) {
|
|
403
|
+
const signature = funcDef.signatures[0];
|
|
404
|
+
if (signature) {
|
|
405
|
+
const params = signature.parameters || [];
|
|
406
|
+
const requiredCount = params.filter(p => !p.optional).length;
|
|
407
|
+
const maxCount = params.length;
|
|
408
|
+
const actualCount = node.arguments.length;
|
|
409
|
+
|
|
410
|
+
if (actualCount < requiredCount) {
|
|
411
|
+
diagnostics.push(
|
|
412
|
+
this.createError(
|
|
413
|
+
node,
|
|
414
|
+
`${functionName} expects at least ${requiredCount} argument${requiredCount !== 1 ? 's' : ''}, got ${actualCount}`,
|
|
415
|
+
ErrorCodes.WRONG_ARGUMENT_COUNT
|
|
416
|
+
)
|
|
417
|
+
);
|
|
418
|
+
hasError = true;
|
|
419
|
+
} else if (actualCount > maxCount) {
|
|
420
|
+
diagnostics.push(
|
|
421
|
+
this.createError(
|
|
422
|
+
node,
|
|
423
|
+
`${functionName} expects at most ${maxCount} argument${maxCount !== 1 ? 's' : ''}, got ${actualCount}`,
|
|
424
|
+
ErrorCodes.WRONG_ARGUMENT_COUNT
|
|
425
|
+
)
|
|
291
426
|
);
|
|
427
|
+
hasError = true;
|
|
292
428
|
}
|
|
293
429
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
430
|
+
}
|
|
431
|
+
return { diagnostics, hasError };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private validateUnionTypeFilters(
|
|
435
|
+
functionName: string,
|
|
436
|
+
node: FunctionNode,
|
|
437
|
+
context: AnalysisContext
|
|
438
|
+
): Diagnostic[] {
|
|
439
|
+
const diagnostics: Diagnostic[] = [];
|
|
440
|
+
if (!['ofType', 'is', 'as'].includes(functionName) || node.arguments.length === 0) {
|
|
441
|
+
return diagnostics;
|
|
442
|
+
}
|
|
443
|
+
const inputType = context.inputType;
|
|
444
|
+
if (!isUnionType(inputType)) {
|
|
445
|
+
return diagnostics;
|
|
446
|
+
}
|
|
447
|
+
const typeArg = node.arguments[0]!;
|
|
448
|
+
let targetType: string | undefined;
|
|
449
|
+
if (typeArg.type === NodeType.Identifier) {
|
|
450
|
+
targetType = (typeArg as IdentifierNode).name;
|
|
451
|
+
}
|
|
452
|
+
if (!targetType) {
|
|
453
|
+
return diagnostics;
|
|
454
|
+
}
|
|
455
|
+
const diag = validateUnionChoice(inputType, targetType, typeArg.range || node.range, 'invalid-type-filter', 'Type');
|
|
456
|
+
if (diag && functionName === 'ofType') diagnostics.push(diag);
|
|
457
|
+
return diagnostics;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private async analyzeArguments(
|
|
461
|
+
funcDef: FunctionDefinition,
|
|
462
|
+
node: FunctionNode,
|
|
463
|
+
context: AnalysisContext,
|
|
464
|
+
functionName: string
|
|
465
|
+
): Promise<{ argTypes: TypeInfo[]; diagnostics: Diagnostic[] }> {
|
|
466
|
+
const diagnostics: Diagnostic[] = [];
|
|
467
|
+
const argTypes: TypeInfo[] = [];
|
|
468
|
+
const signature = funcDef.signatures?.[0];
|
|
469
|
+
const params = signature?.parameters || [];
|
|
470
|
+
|
|
471
|
+
for (let i = 0; i < node.arguments.length; i++) {
|
|
472
|
+
const arg = node.arguments[i]!;
|
|
473
|
+
const param = params[i];
|
|
474
|
+
const isTypeParameter = !!param?.typeReference;
|
|
475
|
+
|
|
476
|
+
if (isTypeParameter) {
|
|
477
|
+
argTypes.push({ type: 'TypeReference' as TypeName, singleton: true });
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (param?.expression) {
|
|
482
|
+
const itemType = { ...context.inputType, singleton: true };
|
|
483
|
+
const exprContext = context
|
|
484
|
+
.withSystemVariable('$this', itemType)
|
|
485
|
+
.withSystemVariable('$index', { type: 'Integer', singleton: true });
|
|
486
|
+
const argResult = await this.analyzeNode(arg, exprContext);
|
|
487
|
+
diagnostics.push(...argResult.diagnostics);
|
|
488
|
+
argTypes.push(argResult.type);
|
|
489
|
+
if (this.stoppedAtCursor) {
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const thisType = context.systemVariables.get('$this') || context.inputType;
|
|
496
|
+
const argContext = context.withInputType(thisType);
|
|
497
|
+
const argResult = await this.analyzeNode(arg, argContext);
|
|
498
|
+
diagnostics.push(...argResult.diagnostics);
|
|
499
|
+
argTypes.push(argResult.type);
|
|
500
|
+
if (this.stoppedAtCursor) {
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return { argTypes, diagnostics };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private matchAndDiagnoseSignature(
|
|
509
|
+
funcDef: FunctionDefinition,
|
|
510
|
+
actualInput: TypeInfo,
|
|
511
|
+
argTypes: TypeInfo[],
|
|
512
|
+
node: FunctionNode,
|
|
513
|
+
functionName: string,
|
|
514
|
+
hasArityError: boolean
|
|
515
|
+
): { match: FunctionSignature | null; diagnostics: Diagnostic[]; earlyReturn?: TypeInfo } {
|
|
516
|
+
const diagnostics: Diagnostic[] = [];
|
|
517
|
+
let match: FunctionSignature | null = null;
|
|
518
|
+
|
|
519
|
+
if (!hasArityError && funcDef.signatures && funcDef.signatures.length > 0) {
|
|
520
|
+
match = matchFunctionSignature(actualInput, argTypes, funcDef) || null;
|
|
521
|
+
|
|
522
|
+
if (!match) {
|
|
523
|
+
const inputIsEmpty = isEmptyCollection(actualInput);
|
|
524
|
+
if (inputIsEmpty && !funcDef.doesNotPropagateEmpty) {
|
|
525
|
+
const sig = funcDef.signatures[0];
|
|
526
|
+
if (sig) {
|
|
527
|
+
diagnostics.push(
|
|
528
|
+
...checkParamTypes(sig, argTypes, node.arguments, {
|
|
529
|
+
warnOnSingletonOnly: false,
|
|
530
|
+
doesNotPropagateEmpty: !!funcDef.doesNotPropagateEmpty,
|
|
531
|
+
treatEmptyAsWarning: true,
|
|
532
|
+
errorCode: ErrorCodes.ARGUMENT_TYPE_MISMATCH,
|
|
533
|
+
})
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
let inputMatchingSignature: FunctionSignature | null = null;
|
|
538
|
+
for (const sig of funcDef.signatures) {
|
|
539
|
+
let inputMatches = true;
|
|
540
|
+
if (sig.input) {
|
|
541
|
+
const expectedInput = sig.input;
|
|
542
|
+
const singletonMatch = !expectedInput.singleton || actualInput.singleton === true;
|
|
543
|
+
const typeMatch =
|
|
544
|
+
expectedInput.type === 'Any' ||
|
|
545
|
+
actualInput.type === 'Any' ||
|
|
546
|
+
expectedInput.type === actualInput.type ||
|
|
547
|
+
(expectedInput.type === 'Decimal' && actualInput.type === 'Integer');
|
|
548
|
+
inputMatches = singletonMatch && typeMatch;
|
|
549
|
+
}
|
|
550
|
+
if (inputMatches) {
|
|
551
|
+
inputMatchingSignature = sig;
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
312
554
|
}
|
|
313
|
-
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
|
|
555
|
+
|
|
556
|
+
if (inputMatchingSignature && inputMatchingSignature.parameters) {
|
|
557
|
+
diagnostics.push(
|
|
558
|
+
...checkParamTypes(inputMatchingSignature, argTypes, node.arguments, {
|
|
559
|
+
warnOnSingletonOnly: true,
|
|
560
|
+
doesNotPropagateEmpty: !!funcDef.doesNotPropagateEmpty,
|
|
561
|
+
errorCode: ErrorCodes.ARGUMENT_TYPE_MISMATCH,
|
|
562
|
+
})
|
|
317
563
|
);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
564
|
+
} else {
|
|
565
|
+
const actualTypeStr = actualInput.singleton ? actualInput.type : `${actualInput.type}[]`;
|
|
566
|
+
const hasSingletonSignature = funcDef.signatures.some(sig => sig.input?.singleton && sig.input.type === actualInput.type);
|
|
567
|
+
const permissive = ['anyFalse', 'anyTrue'];
|
|
568
|
+
if (hasSingletonSignature && !actualInput.singleton) {
|
|
569
|
+
diagnostics.push(
|
|
570
|
+
this.createError(
|
|
571
|
+
node,
|
|
572
|
+
`${functionName} expects a singleton value, but received collection type ${actualTypeStr}`,
|
|
573
|
+
ErrorCodes.SINGLETON_REQUIRED
|
|
574
|
+
)
|
|
575
|
+
);
|
|
576
|
+
} else if (!permissive.includes(functionName)) {
|
|
577
|
+
const expectedTypes = funcDef.signatures
|
|
578
|
+
.map(sig => (sig.input ? (sig.input.singleton ? sig.input.type : `${sig.input.type}[]`) : 'Any'))
|
|
579
|
+
.filter((v, i, a) => a.indexOf(v) === i)
|
|
580
|
+
.join(' or ');
|
|
581
|
+
diagnostics.push(
|
|
582
|
+
this.createError(
|
|
583
|
+
node,
|
|
584
|
+
`Cannot apply ${functionName}() to ${actualTypeStr}. Function expects ${expectedTypes}.`,
|
|
585
|
+
ErrorCodes.INVALID_OPERAND_TYPE
|
|
586
|
+
)
|
|
587
|
+
);
|
|
328
588
|
}
|
|
329
589
|
}
|
|
330
590
|
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const func = registry.getFunction(funcName);
|
|
334
|
-
|
|
335
|
-
if (!func) {
|
|
336
|
-
this.diagnostics.push(
|
|
337
|
-
toDiagnostic(Errors.unknownFunction(funcName, node.range))
|
|
338
|
-
);
|
|
339
591
|
} else {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
toDiagnostic(Errors.wrongArgumentCount(funcName, requiredParams, node.arguments.length, node.range))
|
|
348
|
-
);
|
|
349
|
-
} else if (node.arguments.length > maxParams) {
|
|
350
|
-
this.diagnostics.push(
|
|
351
|
-
toDiagnostic(Errors.wrongArgumentCount(funcName, maxParams, node.arguments.length, node.range))
|
|
592
|
+
if (match.parameters) {
|
|
593
|
+
diagnostics.push(
|
|
594
|
+
...checkParamTypes(match, argTypes, node.arguments, {
|
|
595
|
+
warnOnSingletonOnly: true,
|
|
596
|
+
doesNotPropagateEmpty: !!funcDef.doesNotPropagateEmpty,
|
|
597
|
+
errorCode: ErrorCodes.ARGUMENT_TYPE_MISMATCH,
|
|
598
|
+
})
|
|
352
599
|
);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
600
|
+
} else {
|
|
601
|
+
const permissive = ['anyFalse', 'anyTrue'];
|
|
602
|
+
if (permissive.includes(functionName)) {
|
|
603
|
+
return { match, diagnostics, earlyReturn: { type: 'Boolean', singleton: true } };
|
|
604
|
+
}
|
|
358
605
|
}
|
|
359
606
|
}
|
|
360
607
|
}
|
|
361
|
-
|
|
362
|
-
|
|
608
|
+
|
|
609
|
+
return { match, diagnostics };
|
|
363
610
|
}
|
|
364
611
|
|
|
365
|
-
private
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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);
|
|
612
|
+
private propagatesEmpty(
|
|
613
|
+
funcDef: FunctionDefinition,
|
|
614
|
+
inputType: TypeInfo,
|
|
615
|
+
argTypes: TypeInfo[]
|
|
616
|
+
): boolean {
|
|
617
|
+
if (funcDef.doesNotPropagateEmpty) {
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
const inputIsEmpty = isEmptyCollection(inputType);
|
|
621
|
+
const hasEmptyArgument = argTypes.some(argType => isEmptyCollection(argType));
|
|
622
|
+
return inputIsEmpty || hasEmptyArgument;
|
|
404
623
|
}
|
|
405
624
|
|
|
406
|
-
private
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
}
|
|
625
|
+
private async inferFunctionResultType(
|
|
626
|
+
funcDef: FunctionDefinition,
|
|
627
|
+
node: FunctionNode,
|
|
628
|
+
context: AnalysisContext,
|
|
629
|
+
argTypes: TypeInfo[],
|
|
630
|
+
matchingSignature: FunctionSignature | null
|
|
631
|
+
): Promise<TypeInfo> {
|
|
632
|
+
if (funcDef.inferResultType) {
|
|
633
|
+
return funcDef.inferResultType(this, node, context.inputType);
|
|
634
|
+
}
|
|
635
|
+
if (matchingSignature) {
|
|
636
|
+
return resolveResultType(matchingSignature.result as any, {
|
|
637
|
+
input: context.inputType,
|
|
638
|
+
firstParam: argTypes[0],
|
|
639
|
+
});
|
|
442
640
|
}
|
|
443
|
-
|
|
444
|
-
this.visitNode(node.expression);
|
|
641
|
+
return context.inputType;
|
|
445
642
|
}
|
|
446
643
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
644
|
+
/**
|
|
645
|
+
* Analyzes variable references, checking against context.
|
|
646
|
+
*/
|
|
647
|
+
private analyzeVariable(node: VariableNode, context: AnalysisContext): InternalAnalysisResult {
|
|
648
|
+
const varName = node.name;
|
|
649
|
+
const diagnostics: Diagnostic[] = [];
|
|
650
|
+
|
|
651
|
+
// Check if it's a user variable (starts with %)
|
|
652
|
+
if (varName.startsWith('%')) {
|
|
653
|
+
// Special handling for %context - it's a built-in environment variable
|
|
654
|
+
// that always returns the original input to the evaluation engine
|
|
655
|
+
if (varName === '%context') {
|
|
656
|
+
// %context returns the root input type (the original input to evaluate())
|
|
657
|
+
// In the analyzer, we track this as the initial input type
|
|
658
|
+
return { type: context.inputType, diagnostics, context };
|
|
461
659
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
vars.add(nameArg.value as string);
|
|
660
|
+
|
|
661
|
+
const name = varName.substring(1); // Remove % prefix
|
|
662
|
+
const varType = context.userVariables.get(name);
|
|
663
|
+
|
|
664
|
+
if (!varType) {
|
|
665
|
+
// If we have dynamic variables in scope, we can't be sure this is an error
|
|
666
|
+
if (context.hasDynamicVariables) {
|
|
667
|
+
diagnostics.push(this.createWarning(node, `Variable '${varName}' may not be defined (dynamic variables in scope)`));
|
|
668
|
+
// Return Any type since we don't know the actual type
|
|
669
|
+
return { type: { type: 'Any', singleton: false }, diagnostics };
|
|
670
|
+
} else {
|
|
671
|
+
// No dynamic variables, so this is definitely an error
|
|
672
|
+
diagnostics.push(this.createError(node, Errors.unknownUserVariable(varName).message, ErrorCodes.UNKNOWN_USER_VARIABLE));
|
|
673
|
+
return { type: { type: 'Any', singleton: false }, diagnostics };
|
|
477
674
|
}
|
|
478
675
|
}
|
|
676
|
+
|
|
677
|
+
// Attach type info to the node for backward compatibility
|
|
678
|
+
node.typeInfo = varType;
|
|
679
|
+
return { type: varType, diagnostics, context };
|
|
479
680
|
}
|
|
480
|
-
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
}
|
|
681
|
+
|
|
682
|
+
// Check system variables
|
|
683
|
+
const sysVarType = context.systemVariables.get(varName);
|
|
684
|
+
if (sysVarType) {
|
|
685
|
+
// Attach type info to the node for backward compatibility
|
|
686
|
+
node.typeInfo = sysVarType;
|
|
687
|
+
return { type: sysVarType, diagnostics, context };
|
|
502
688
|
}
|
|
503
|
-
|
|
504
|
-
|
|
689
|
+
|
|
690
|
+
// Unknown variable
|
|
691
|
+
diagnostics.push(this.createError(node, `Unknown variable: ${varName}`, ErrorCodes.UNKNOWN_VARIABLE));
|
|
692
|
+
return { type: { type: 'Any', singleton: false }, diagnostics };
|
|
505
693
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Analyzes identifier nodes (property access).
|
|
697
|
+
*/
|
|
698
|
+
private async analyzeIdentifier(node: IdentifierNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
|
|
699
|
+
const name = 'name' in node ? node.name : '';
|
|
700
|
+
const diagnostics: Diagnostic[] = [];
|
|
701
|
+
|
|
702
|
+
// Try to use model provider for accurate type information
|
|
703
|
+
if (context.modelProvider) {
|
|
704
|
+
// First try to navigate from input type (property access)
|
|
705
|
+
const elementType = await context.modelProvider.getElementType(context.inputType, name);
|
|
706
|
+
if (elementType) {
|
|
707
|
+
return {
|
|
708
|
+
type: elementType,
|
|
709
|
+
diagnostics,
|
|
710
|
+
context
|
|
711
|
+
};
|
|
533
712
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
}
|
|
713
|
+
// Chain-head rule: at the head of a navigation chain, allow treating the
|
|
714
|
+
// identifier as a known type to seed the chain (e.g., Patient.name)
|
|
715
|
+
if ((context as any)._chainHead === true) {
|
|
716
|
+
const typeInfo = await context.modelProvider.getType(name);
|
|
717
|
+
if (typeInfo) {
|
|
718
|
+
return {
|
|
719
|
+
type: typeInfo,
|
|
720
|
+
diagnostics,
|
|
721
|
+
context
|
|
722
|
+
};
|
|
566
723
|
}
|
|
567
724
|
}
|
|
725
|
+
|
|
726
|
+
// If property not found and we have a concrete non-union type, report warning
|
|
727
|
+
// FHIRPath returns empty for unknown properties, not an error
|
|
728
|
+
const mc: any = context.inputType.modelContext;
|
|
729
|
+
const isUnion = !!(mc && typeof mc === 'object' && 'isUnion' in mc && mc.isUnion);
|
|
730
|
+
if (context.inputType.namespace && context.inputType.name && context.inputType.modelContext && !isUnion) {
|
|
731
|
+
const typeStr = `${context.inputType.namespace}.${context.inputType.name}`;
|
|
732
|
+
diagnostics.push(toDiagnostic(Errors.unknownProperty(name, typeStr, node.range), DiagnosticSeverity.Warning));
|
|
733
|
+
return {
|
|
734
|
+
type: { type: 'Any', singleton: false },
|
|
735
|
+
diagnostics,
|
|
736
|
+
context
|
|
737
|
+
};
|
|
738
|
+
}
|
|
568
739
|
}
|
|
569
740
|
|
|
570
|
-
|
|
741
|
+
// Without a model provider, we can't know the type
|
|
742
|
+
// Return Any type - don't make assumptions
|
|
743
|
+
return {
|
|
744
|
+
type: { type: 'Any', singleton: false },
|
|
745
|
+
diagnostics,
|
|
746
|
+
context
|
|
747
|
+
};
|
|
571
748
|
}
|
|
572
749
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
return this.inferErrorNodeType(node as ErrorNode, inputType);
|
|
579
|
-
}
|
|
750
|
+
/**
|
|
751
|
+
* Analyzes literal values.
|
|
752
|
+
*/
|
|
753
|
+
private analyzeLiteral(node: LiteralNode, context: AnalysisContext): InternalAnalysisResult {
|
|
754
|
+
let type: TypeInfo;
|
|
580
755
|
|
|
581
|
-
switch (node.type) {
|
|
582
|
-
case NodeType.Literal:
|
|
583
|
-
return this.inferLiteralType(node as LiteralNode);
|
|
584
|
-
|
|
585
|
-
case NodeType.Binary:
|
|
586
|
-
return await this.inferBinaryType(node as BinaryNode, inputType);
|
|
587
|
-
|
|
588
|
-
case NodeType.Unary:
|
|
589
|
-
return this.inferUnaryType(node as UnaryNode);
|
|
590
|
-
|
|
591
|
-
case NodeType.Function:
|
|
592
|
-
return await this.inferFunctionType(node as FunctionNode, inputType);
|
|
593
|
-
|
|
594
|
-
case NodeType.Identifier:
|
|
595
|
-
return await this.inferIdentifierType(node as IdentifierNode, inputType);
|
|
596
|
-
|
|
597
|
-
case NodeType.Variable:
|
|
598
|
-
return this.inferVariableType(node as VariableNode);
|
|
599
|
-
|
|
600
|
-
case NodeType.Collection:
|
|
601
|
-
return await this.inferCollectionType(node as CollectionNode);
|
|
602
|
-
|
|
603
|
-
case NodeType.TypeCast:
|
|
604
|
-
return await this.inferTypeCastType(node as TypeCastNode);
|
|
605
|
-
|
|
606
|
-
case NodeType.MembershipTest:
|
|
607
|
-
return { type: 'Boolean', singleton: true };
|
|
608
|
-
|
|
609
|
-
case NodeType.TypeOrIdentifier:
|
|
610
|
-
return await this.inferTypeOrIdentifierType(node as TypeOrIdentifierNode, inputType);
|
|
611
|
-
|
|
612
|
-
default:
|
|
613
|
-
return { type: 'Any', singleton: false };
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
private inferErrorNodeType(errorNode: ErrorNode, inputType?: TypeInfo): TypeInfo {
|
|
618
|
-
// For error nodes, return a generic type that allows partial analysis to continue
|
|
619
|
-
// This enables type checking for valid parts of broken expressions
|
|
620
|
-
return { type: 'Any', singleton: false };
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
private inferLiteralType(node: LiteralNode): TypeInfo {
|
|
624
756
|
switch (node.valueType) {
|
|
625
757
|
case 'string':
|
|
626
|
-
|
|
758
|
+
type = { type: 'String', singleton: true };
|
|
759
|
+
break;
|
|
627
760
|
case 'number':
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
761
|
+
// Number without decimal point is integer
|
|
762
|
+
type = { type: 'Integer', singleton: true };
|
|
763
|
+
break;
|
|
764
|
+
case 'decimal':
|
|
765
|
+
// Number with decimal point is decimal (even if value is whole)
|
|
766
|
+
type = { type: 'Decimal', singleton: true };
|
|
767
|
+
break;
|
|
633
768
|
case 'boolean':
|
|
634
|
-
|
|
769
|
+
type = { type: 'Boolean', singleton: true };
|
|
770
|
+
break;
|
|
635
771
|
case 'date':
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
return { type: 'DateTime', singleton: true };
|
|
772
|
+
type = { type: 'Date', singleton: true };
|
|
773
|
+
break;
|
|
639
774
|
case 'time':
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
775
|
+
type = { type: 'Time', singleton: true };
|
|
776
|
+
break;
|
|
777
|
+
case 'datetime':
|
|
778
|
+
type = { type: 'DateTime', singleton: true };
|
|
779
|
+
break;
|
|
643
780
|
default:
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
private async inferBinaryType(node: BinaryNode, inputType?: TypeInfo): Promise<TypeInfo> {
|
|
649
|
-
const operator = registry.getOperatorDefinition(node.operator);
|
|
650
|
-
if (!operator) {
|
|
651
|
-
return { type: 'Any', singleton: false };
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// For navigation (dot operator), we need special handling
|
|
655
|
-
if (node.operator === '.') {
|
|
656
|
-
return await this.inferNavigationType(node, inputType);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// Infer types of operands
|
|
660
|
-
const leftType = await this.inferType(node.left, inputType);
|
|
661
|
-
const rightType = await this.inferType(node.right, inputType);
|
|
662
|
-
|
|
663
|
-
// Find matching signature
|
|
664
|
-
for (const sig of operator.signatures) {
|
|
665
|
-
if (this.isTypeCompatible(leftType, sig.left) &&
|
|
666
|
-
this.isTypeCompatible(rightType, sig.right)) {
|
|
667
|
-
return this.resolveResultType(sig.result, inputType, leftType, rightType);
|
|
668
|
-
}
|
|
781
|
+
type = { type: 'Any', singleton: true };
|
|
669
782
|
}
|
|
670
783
|
|
|
671
|
-
|
|
672
|
-
const defaultResult = operator.signatures[0]?.result || { type: 'Any', singleton: false };
|
|
673
|
-
return this.resolveResultType(defaultResult, inputType, leftType, rightType);
|
|
784
|
+
return { type, diagnostics: [] };
|
|
674
785
|
}
|
|
675
786
|
|
|
676
|
-
private
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
// If the right side is a function, return the function's type
|
|
680
|
-
if (node.right.type === NodeType.Function) {
|
|
681
|
-
return await this.inferType(node.right, leftType);
|
|
682
|
-
}
|
|
787
|
+
private analyzeTemporalLiteral(node: TemporalLiteralNode, context: AnalysisContext): InternalAnalysisResult {
|
|
788
|
+
let type: TypeInfo;
|
|
683
789
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
if (leftType.namespace && leftType.name && leftType.modelContext &&
|
|
697
|
-
!(leftType.modelContext as any).isUnion) {
|
|
698
|
-
this.diagnostics.push(
|
|
699
|
-
toDiagnostic(Errors.unknownProperty(propertyName, `${leftType.namespace}.${leftType.name}`, node.right.range))
|
|
700
|
-
);
|
|
701
|
-
}
|
|
790
|
+
switch (node.valueType) {
|
|
791
|
+
case 'date':
|
|
792
|
+
type = { type: 'Date', singleton: true };
|
|
793
|
+
break;
|
|
794
|
+
case 'time':
|
|
795
|
+
type = { type: 'Time', singleton: true };
|
|
796
|
+
break;
|
|
797
|
+
case 'datetime':
|
|
798
|
+
type = { type: 'DateTime', singleton: true };
|
|
799
|
+
break;
|
|
800
|
+
default:
|
|
801
|
+
type = { type: 'Any', singleton: true };
|
|
702
802
|
}
|
|
703
803
|
|
|
704
|
-
|
|
705
|
-
return { type: 'Any', singleton: false };
|
|
804
|
+
return { type, diagnostics: [] };
|
|
706
805
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
if (
|
|
717
|
-
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Analyzes unary operators.
|
|
809
|
+
*/
|
|
810
|
+
private async analyzeUnary(node: UnaryNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
|
|
811
|
+
const operandResult = await this.analyzeNode(node.operand, context);
|
|
812
|
+
const diagnostics: Diagnostic[] = [...operandResult.diagnostics];
|
|
813
|
+
const opDef = registry.getOperatorDefinition(node.operator);
|
|
814
|
+
|
|
815
|
+
if (opDef && opDef.signatures && opDef.signatures.length > 0) {
|
|
816
|
+
// Use first signature's result if defined
|
|
817
|
+
const sig = opDef.signatures[0]!;
|
|
818
|
+
const resultType = typeof sig.result === 'object' ? sig.result : operandResult.type;
|
|
819
|
+
return { type: resultType, diagnostics };
|
|
718
820
|
}
|
|
719
|
-
|
|
720
|
-
|
|
821
|
+
|
|
822
|
+
// Fallback: preserve operand type
|
|
823
|
+
return { type: operandResult.type, diagnostics };
|
|
721
824
|
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
if
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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
|
-
|
|
810
|
-
// Special handling for functions with dynamic result types
|
|
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') {
|
|
821
|
-
// Functions like where() return the same type as input but always as collection
|
|
822
|
-
return inputType ? { ...inputType, singleton: false } : { type: 'Any', singleton: false };
|
|
823
|
-
} else if (matchingSignature.result === 'inputTypeSingleton') {
|
|
824
|
-
// Functions like first(), last() return the same type as input but as singleton
|
|
825
|
-
return inputType ? { ...inputType, singleton: true } : { type: 'Any', singleton: true };
|
|
826
|
-
} else if (matchingSignature.result === 'parameterType' && node.arguments.length > 0) {
|
|
827
|
-
// Functions like select() return the type of the first parameter expression as collection
|
|
828
|
-
const paramType = await this.inferType(node.arguments[0]!, inputType);
|
|
829
|
-
return { ...paramType, singleton: false };
|
|
830
|
-
} else if (typeof matchingSignature.result === 'object') {
|
|
831
|
-
return matchingSignature.result;
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Analyzes index operations.
|
|
828
|
+
*/
|
|
829
|
+
private async analyzeIndex(node: IndexNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
|
|
830
|
+
const exprResult = await this.analyzeNode(node.expression, context);
|
|
831
|
+
const indexResult = await this.analyzeNode(node.index, context);
|
|
832
|
+
const diagnostics: Diagnostic[] = [...exprResult.diagnostics, ...indexResult.diagnostics];
|
|
833
|
+
|
|
834
|
+
// Index should be Integer singleton; if unknown, skip strict error
|
|
835
|
+
const idxType = indexResult.type;
|
|
836
|
+
const isIntegerSingleton = idxType.type === 'Integer' && idxType.singleton === true;
|
|
837
|
+
if (!isIntegerSingleton && idxType.type !== 'Any') {
|
|
838
|
+
diagnostics.push(this.createError(node.index, 'Index must be an Integer singleton', ErrorCodes.ARGUMENT_TYPE_MISMATCH));
|
|
832
839
|
}
|
|
833
|
-
|
|
834
|
-
|
|
840
|
+
|
|
841
|
+
// Result is the element type (singleton) of the expression collection
|
|
842
|
+
const exprType = exprResult.type;
|
|
843
|
+
const resultType = { ...exprType, singleton: true };
|
|
844
|
+
return { type: resultType, diagnostics };
|
|
835
845
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
// Only check if it's a type name if it starts with uppercase (FHIR convention)
|
|
847
|
-
// or if there's no input type context
|
|
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);
|
|
853
|
-
if (typeInfo) {
|
|
854
|
-
return typeInfo;
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
return { type: 'Any', singleton: false };
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Analyzes quantity literals.
|
|
849
|
+
*/
|
|
850
|
+
private analyzeQuantity(node: QuantityNode, context: AnalysisContext): InternalAnalysisResult {
|
|
851
|
+
return {
|
|
852
|
+
type: { type: 'Quantity', singleton: true },
|
|
853
|
+
diagnostics: [],
|
|
854
|
+
context
|
|
855
|
+
};
|
|
859
856
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Analyzes collection literals.
|
|
860
|
+
*/
|
|
861
|
+
private async analyzeCollection(node: CollectionNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
|
|
862
|
+
const diagnostics: Diagnostic[] = [];
|
|
863
|
+
const isEmpty = node.elements.length === 0;
|
|
864
|
+
const elementTypes: TypeInfo[] = [];
|
|
865
|
+
|
|
866
|
+
for (const element of node.elements) {
|
|
867
|
+
const elemResult = await this.analyzeNode(element, context);
|
|
868
|
+
diagnostics.push(...elemResult.diagnostics);
|
|
869
|
+
elementTypes.push(elemResult.type);
|
|
870
|
+
if (this.stoppedAtCursor) {
|
|
871
|
+
return { type: { type: 'Any', singleton: false }, diagnostics };
|
|
869
872
|
}
|
|
870
873
|
}
|
|
871
|
-
|
|
872
|
-
//
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
874
|
+
|
|
875
|
+
// Infer common element type
|
|
876
|
+
let elementType: TypeInfo = { type: 'Any', singleton: true };
|
|
877
|
+
if (!isEmpty) {
|
|
878
|
+
elementType = elementTypes[0]!;
|
|
879
|
+
for (let i = 1; i < elementTypes.length; i++) {
|
|
880
|
+
const t = elementTypes[i]!;
|
|
881
|
+
if (elementType.type === t.type) {
|
|
882
|
+
// keep; if any is non-singleton, result stays collection anyway
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
// Promote Integer to Decimal when mixed
|
|
886
|
+
if ((elementType.type === 'Integer' && t.type === 'Decimal') || (elementType.type === 'Decimal' && t.type === 'Integer')) {
|
|
887
|
+
elementType = { type: 'Decimal', singleton: true };
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
// Unknown/heterogeneous → Any
|
|
891
|
+
elementType = { type: 'Any', singleton: true };
|
|
892
|
+
break;
|
|
880
893
|
}
|
|
881
894
|
}
|
|
882
|
-
|
|
883
|
-
return { type:
|
|
895
|
+
|
|
896
|
+
return { type: { ...elementType, singleton: false, isEmpty }, diagnostics };
|
|
884
897
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
// User-defined variables - check with or without % prefix
|
|
912
|
-
let varName = node.name;
|
|
913
|
-
if (varName.startsWith('%')) {
|
|
914
|
-
varName = varName.substring(1);
|
|
915
|
-
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Analyzes membership test (is operator).
|
|
901
|
+
*/
|
|
902
|
+
private async analyzeMembershipTest(node: MembershipTestNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
|
|
903
|
+
const exprResult = await this.analyzeNode(node.expression, context);
|
|
904
|
+
const diagnostics = [...exprResult.diagnostics];
|
|
905
|
+
|
|
906
|
+
// ModelProvider requirement for non-primitive target types
|
|
907
|
+
const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
|
|
916
908
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
909
|
+
// Normalize System.X types to check if they're primitive
|
|
910
|
+
let targetType = node.targetType;
|
|
911
|
+
if (targetType.startsWith('System.')) {
|
|
912
|
+
targetType = targetType.substring(7); // Remove "System." prefix
|
|
920
913
|
}
|
|
921
914
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
private async inferCollectionType(node: CollectionNode): Promise<TypeInfo> {
|
|
926
|
-
if (node.elements.length === 0) {
|
|
927
|
-
return { type: 'Any', singleton: false };
|
|
915
|
+
if (!context.modelProvider && !primitiveTypes.includes(targetType)) {
|
|
916
|
+
diagnostics.push(toDiagnostic(Errors.modelProviderRequired('is', node.range)));
|
|
928
917
|
}
|
|
929
918
|
|
|
930
|
-
//
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
if (allSameType) {
|
|
943
|
-
return { ...firstType, singleton: false };
|
|
919
|
+
// Check if testing against a union type
|
|
920
|
+
if (isUnionType(exprResult.type)) {
|
|
921
|
+
const targetType = node.targetType;
|
|
922
|
+
const choices = getUnionChoices(exprResult.type);
|
|
923
|
+
if (choices.length > 0 && !choices.includes(targetType)) {
|
|
924
|
+
diagnostics.push({
|
|
925
|
+
severity: DiagnosticSeverity.Warning,
|
|
926
|
+
code: 'invalid-type-test',
|
|
927
|
+
message: `Type test 'is ${targetType}' will always be false - type not present in union. Available types: ${choices.join(', ')}`,
|
|
928
|
+
range: node.range
|
|
929
|
+
});
|
|
944
930
|
}
|
|
945
931
|
}
|
|
946
932
|
|
|
947
|
-
|
|
948
|
-
|
|
933
|
+
return {
|
|
934
|
+
type: { type: 'Boolean', singleton: true },
|
|
935
|
+
diagnostics
|
|
936
|
+
};
|
|
949
937
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Analyzes type cast (as operator).
|
|
941
|
+
*/
|
|
942
|
+
private async analyzeTypeCast(node: TypeCastNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
|
|
943
|
+
const exprResult = await this.analyzeNode(node.expression, context);
|
|
944
|
+
const diagnostics = [...exprResult.diagnostics];
|
|
945
|
+
|
|
946
|
+
// ModelProvider requirement for non-primitive target types
|
|
947
|
+
const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
|
|
948
|
+
if (!context.modelProvider && !primitiveTypes.includes(node.targetType)) {
|
|
949
|
+
diagnostics.push(toDiagnostic(Errors.modelProviderRequired('as', node.range)));
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// Check if casting from a union type
|
|
953
|
+
if (isUnionType(exprResult.type)) {
|
|
954
|
+
const targetTypeName = node.targetType;
|
|
955
|
+
const choices = getUnionChoices(exprResult.type);
|
|
956
|
+
if (choices.length > 0 && !choices.includes(targetTypeName)) {
|
|
957
|
+
diagnostics.push({
|
|
958
|
+
severity: DiagnosticSeverity.Warning,
|
|
959
|
+
code: 'invalid-type-cast',
|
|
960
|
+
message: `Type cast 'as ${targetTypeName}' may fail - type not present in union. Available types: ${choices.join(', ')}`,
|
|
961
|
+
range: node.range
|
|
962
|
+
});
|
|
959
963
|
}
|
|
960
964
|
}
|
|
961
965
|
|
|
962
|
-
//
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
}
|
|
966
|
+
// Type cast changes the type
|
|
967
|
+
const targetType: TypeInfo = {
|
|
968
|
+
type: node.targetType as TypeName,
|
|
969
|
+
singleton: exprResult.type.singleton
|
|
970
|
+
};
|
|
967
971
|
|
|
968
|
-
return {
|
|
972
|
+
return {
|
|
973
|
+
type: targetType,
|
|
974
|
+
diagnostics
|
|
975
|
+
};
|
|
969
976
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
977
|
+
|
|
978
|
+
/**
|
|
979
|
+
* Analyzes error nodes.
|
|
980
|
+
*/
|
|
981
|
+
private analyzeError(node: ErrorNode, context: AnalysisContext): InternalAnalysisResult {
|
|
982
|
+
return {
|
|
983
|
+
type: { type: 'Any', singleton: false },
|
|
984
|
+
diagnostics: [this.createError(node, node.message, ErrorCodes.INVALID_SYNTAX)]
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Analyzes cursor nodes for completion.
|
|
990
|
+
*/
|
|
991
|
+
private analyzeCursorNode(node: AnyCursorNode, context: AnalysisContext): InternalAnalysisResult {
|
|
992
|
+
// Store cursor context for completion
|
|
993
|
+
if (this.cursorMode) {
|
|
994
|
+
this.stoppedAtCursor = true;
|
|
995
|
+
this.cursorContext = {
|
|
996
|
+
typeBeforeCursor: context.inputType,
|
|
997
|
+
cursorNode: node,
|
|
998
|
+
expectedType: undefined,
|
|
999
|
+
functionCall: undefined
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
// Set expected type based on cursor context
|
|
1003
|
+
if (node.context === CursorContext.Index) {
|
|
1004
|
+
// Index expects an integer
|
|
1005
|
+
this.cursorContext.expectedType = { type: 'Integer', singleton: true };
|
|
1006
|
+
} else if (node.context === CursorContext.Argument) {
|
|
1007
|
+
// Arguments context - check if we're in a function
|
|
1008
|
+
const parent = (node as any).parent;
|
|
1009
|
+
if (parent && parent.type === NodeType.Function) {
|
|
1010
|
+
const funcNode = parent as FunctionNode;
|
|
1011
|
+
if (funcNode.name.type === NodeType.Identifier) {
|
|
1012
|
+
const funcName = (funcNode.name as IdentifierNode).name;
|
|
1013
|
+
const funcDef = registry.getFunction(funcName);
|
|
1014
|
+
if (funcDef) {
|
|
1015
|
+
// Find argument index
|
|
1016
|
+
const argIndex = funcNode.arguments.indexOf(node);
|
|
1017
|
+
this.cursorContext.functionCall = {
|
|
1018
|
+
definition: funcDef,
|
|
1019
|
+
argumentIndex: argIndex >= 0 ? argIndex : 0
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1001
1024
|
}
|
|
1002
1025
|
}
|
|
1003
1026
|
|
|
1004
|
-
return
|
|
1027
|
+
return {
|
|
1028
|
+
type: { type: 'Any', singleton: false },
|
|
1029
|
+
diagnostics: []
|
|
1030
|
+
};
|
|
1005
1031
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1032
|
+
|
|
1033
|
+
// Legacy union combiner removed; union handled in analyzeBinary
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Helper to create diagnostic errors.
|
|
1037
|
+
*/
|
|
1038
|
+
private createError(node: ASTNode, message: string, code?: string): Diagnostic {
|
|
1039
|
+
return {
|
|
1040
|
+
range: node.range,
|
|
1041
|
+
message,
|
|
1042
|
+
severity: DiagnosticSeverity.Error,
|
|
1043
|
+
code,
|
|
1044
|
+
source: 'fhirpath'
|
|
1045
|
+
};
|
|
1018
1046
|
}
|
|
1019
1047
|
|
|
1020
|
-
private
|
|
1021
|
-
return
|
|
1048
|
+
private createWarning(node: ASTNode, message: string, code?: string): Diagnostic {
|
|
1049
|
+
return {
|
|
1050
|
+
range: node.range,
|
|
1051
|
+
message,
|
|
1052
|
+
severity: DiagnosticSeverity.Warning,
|
|
1053
|
+
code,
|
|
1054
|
+
source: 'fhirpath'
|
|
1055
|
+
};
|
|
1022
1056
|
}
|
|
1023
|
-
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Helper method to infer TypeInfo from runtime values (used for user variables).
|
|
1060
|
+
*/
|
|
1024
1061
|
private inferValueType(value: any): TypeInfo {
|
|
1025
1062
|
if (Array.isArray(value)) {
|
|
1026
1063
|
if (value.length === 0) {
|
|
@@ -1034,7 +1071,9 @@ export class Analyzer {
|
|
|
1034
1071
|
if (typeof value === 'string') {
|
|
1035
1072
|
return { type: 'String', singleton: true };
|
|
1036
1073
|
} else if (typeof value === 'number') {
|
|
1037
|
-
return
|
|
1074
|
+
return Number.isInteger(value)
|
|
1075
|
+
? { type: 'Integer', singleton: true }
|
|
1076
|
+
: { type: 'Decimal', singleton: true };
|
|
1038
1077
|
} else if (typeof value === 'boolean') {
|
|
1039
1078
|
return { type: 'Boolean', singleton: true };
|
|
1040
1079
|
} else if (value instanceof Date) {
|
|
@@ -1043,359 +1082,55 @@ export class Analyzer {
|
|
|
1043
1082
|
return { type: 'Any', singleton: true };
|
|
1044
1083
|
}
|
|
1045
1084
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1085
|
+
|
|
1086
|
+
async analyze(
|
|
1087
|
+
ast: ASTNode,
|
|
1088
|
+
userVariables?: Record<string, any>,
|
|
1049
1089
|
inputType?: TypeInfo,
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
switch (resultSpec) {
|
|
1058
|
-
case 'inputType':
|
|
1059
|
-
return inputType || { type: 'Any', singleton: false };
|
|
1060
|
-
case 'leftType':
|
|
1061
|
-
// For union-like operators, result is always a collection
|
|
1062
|
-
return leftType ? { ...leftType, singleton: false } : { type: 'Any', singleton: false };
|
|
1063
|
-
case 'rightType':
|
|
1064
|
-
return rightType ? { ...rightType, singleton: false } : { type: 'Any', singleton: false };
|
|
1065
|
-
default:
|
|
1066
|
-
return { type: 'Any', singleton: false };
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
private checkBinaryOperatorTypes(node: BinaryNode, operator: import('./types').OperatorDefinition): void {
|
|
1071
|
-
const leftType = node.left.typeInfo!;
|
|
1072
|
-
const rightType = node.right.typeInfo!;
|
|
1073
|
-
|
|
1074
|
-
// Find if any signature matches
|
|
1075
|
-
let foundMatch = false;
|
|
1076
|
-
for (const sig of operator.signatures) {
|
|
1077
|
-
if (this.isTypeCompatible(leftType, sig.left) &&
|
|
1078
|
-
this.isTypeCompatible(rightType, sig.right)) {
|
|
1079
|
-
foundMatch = true;
|
|
1080
|
-
break;
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1090
|
+
options?: AnalyzerOptions
|
|
1091
|
+
): Promise<AnalysisResultWithCursor> {
|
|
1092
|
+
this.cursorMode = options?.cursorMode ?? false;
|
|
1093
|
+
this.stoppedAtCursor = false;
|
|
1094
|
+
this.cursorContext = undefined;
|
|
1083
1095
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
private checkFunctionArgumentTypes(node: FunctionNode, func: import('./types').FunctionDefinition): void {
|
|
1094
|
-
const params = func.signatures?.[0]?.parameters || [];
|
|
1096
|
+
// Create initial context with system and user variables
|
|
1097
|
+
const systemVars = new Map<string, TypeInfo>();
|
|
1098
|
+
// $this should be the input type (the root context), not Any
|
|
1099
|
+
systemVars.set('$this', inputType || { type: 'Any', singleton: false });
|
|
1100
|
+
systemVars.set('$index', { type: 'Integer', singleton: true });
|
|
1101
|
+
systemVars.set('$total', { type: 'Any', singleton: false });
|
|
1095
1102
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
if (!this.isTypeCompatible(arg.typeInfo, param.type)) {
|
|
1103
|
-
const argTypeStr = this.typeToString(arg.typeInfo);
|
|
1104
|
-
const paramTypeStr = this.typeToString(param.type);
|
|
1105
|
-
this.diagnostics.push(
|
|
1106
|
-
toDiagnostic(Errors.argumentTypeMismatch(i + 1, func.name, paramTypeStr, argTypeStr, arg.range))
|
|
1107
|
-
);
|
|
1103
|
+
const userVars = new Map<string, TypeInfo>();
|
|
1104
|
+
if (userVariables) {
|
|
1105
|
+
Object.keys(userVariables).forEach(name => {
|
|
1106
|
+
const value = userVariables[name];
|
|
1107
|
+
if (value !== undefined && value !== null) {
|
|
1108
|
+
userVars.set(name, this.inferValueType(value));
|
|
1108
1109
|
}
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
private typeToString(type: TypeInfo): string {
|
|
1114
|
-
const singletonStr = type.singleton ? '' : '[]';
|
|
1115
|
-
if (type.namespace && type.name) {
|
|
1116
|
-
return `${type.namespace}.${type.name}${singletonStr}`;
|
|
1110
|
+
});
|
|
1117
1111
|
}
|
|
1118
|
-
return `${type.type}${singletonStr}`;
|
|
1119
|
-
}
|
|
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
1112
|
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
-
|
|
1155
|
-
/**
|
|
1156
|
-
* Annotate AST with type information
|
|
1157
|
-
*/
|
|
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
|
-
}
|
|
1113
|
+
// Create context with analyzeNode callback and model provider
|
|
1114
|
+
const initialContext = new AnalysisContext(
|
|
1115
|
+
inputType || { type: 'Any', singleton: false },
|
|
1116
|
+
systemVars,
|
|
1117
|
+
userVars,
|
|
1118
|
+
(node, ctx) => this.analyzeNode(node, ctx),
|
|
1119
|
+
this.modelProvider
|
|
1120
|
+
);
|
|
1171
1121
|
|
|
1172
|
-
//
|
|
1173
|
-
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1122
|
+
// Run context-flow analysis
|
|
1123
|
+
const result = await this.analyzeNode(ast, initialContext);
|
|
1176
1124
|
|
|
1177
|
-
//
|
|
1178
|
-
if (node.type === 'Error') {
|
|
1179
|
-
const errorNode = node as ErrorNode;
|
|
1180
|
-
// Infer a reasonable type for error nodes
|
|
1181
|
-
node.typeInfo = this.inferErrorNodeType(errorNode, inputType);
|
|
1182
|
-
// Add diagnostic for the error
|
|
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
|
-
});
|
|
1190
|
-
return;
|
|
1191
|
-
}
|
|
1125
|
+
// Legacy annotateAST/visitor path removed from default analysis to avoid duplication.
|
|
1192
1126
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
-
}
|
|
1219
|
-
|
|
1220
|
-
// For navigation, pass the left's type as input to the right
|
|
1221
|
-
if (binaryNode.operator === '.') {
|
|
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
|
-
}
|
|
1252
|
-
} else {
|
|
1253
|
-
await this.annotateAST(binaryNode.right, inputType);
|
|
1254
|
-
}
|
|
1255
|
-
break;
|
|
1256
|
-
|
|
1257
|
-
case NodeType.Unary:
|
|
1258
|
-
const unaryNode = node as UnaryNode;
|
|
1259
|
-
await this.annotateAST(unaryNode.operand, inputType);
|
|
1260
|
-
break;
|
|
1261
|
-
|
|
1262
|
-
case NodeType.Function:
|
|
1263
|
-
const funcNode = node as FunctionNode;
|
|
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
|
-
}
|
|
1368
|
-
break;
|
|
1369
|
-
|
|
1370
|
-
case NodeType.Collection:
|
|
1371
|
-
const collNode = node as CollectionNode;
|
|
1372
|
-
for (const el of collNode.elements) {
|
|
1373
|
-
await this.annotateAST(el, inputType);
|
|
1374
|
-
if (this.stoppedAtCursor) break;
|
|
1375
|
-
}
|
|
1376
|
-
break;
|
|
1377
|
-
|
|
1378
|
-
case NodeType.TypeCast:
|
|
1379
|
-
const castNode = node as TypeCastNode;
|
|
1380
|
-
await this.annotateAST(castNode.expression, inputType);
|
|
1381
|
-
break;
|
|
1382
|
-
|
|
1383
|
-
case NodeType.MembershipTest:
|
|
1384
|
-
const memberNode = node as MembershipTestNode;
|
|
1385
|
-
await this.annotateAST(memberNode.expression, inputType);
|
|
1386
|
-
break;
|
|
1387
|
-
|
|
1388
|
-
case NodeType.Index:
|
|
1389
|
-
const indexNode = node as IndexNode;
|
|
1390
|
-
await this.annotateAST(indexNode.expression, inputType);
|
|
1391
|
-
if (!this.stoppedAtCursor) {
|
|
1392
|
-
await this.annotateAST(indexNode.index, inputType);
|
|
1393
|
-
}
|
|
1394
|
-
break;
|
|
1395
|
-
|
|
1396
|
-
case NodeType.TypeOrIdentifier:
|
|
1397
|
-
// TypeOrIdentifier doesn't have children to annotate
|
|
1398
|
-
break;
|
|
1399
|
-
}
|
|
1127
|
+
return {
|
|
1128
|
+
diagnostics: result.diagnostics,
|
|
1129
|
+
ast,
|
|
1130
|
+
type: result.type,
|
|
1131
|
+
userVariables: new Map(result.context?.userVariables || initialContext.userVariables),
|
|
1132
|
+
stoppedAtCursor: this.cursorMode ? this.stoppedAtCursor : undefined,
|
|
1133
|
+
cursorContext: this.cursorMode ? this.cursorContext : undefined
|
|
1134
|
+
};
|
|
1400
1135
|
}
|
|
1401
|
-
}
|
|
1136
|
+
}
|