@atomic-ehr/fhirpath 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +716 -238
- package/dist/index.d.ts +225 -119
- package/dist/index.js +10911 -5600
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
- package/src/analyzer/augmentor.ts +242 -0
- package/src/analyzer/cursor-services.ts +75 -0
- package/src/analyzer/scope-manager.ts +57 -0
- package/src/analyzer/trivia-indexer.ts +58 -0
- package/src/analyzer/type-compat.ts +157 -0
- package/src/analyzer/utils.ts +132 -0
- package/src/analyzer.ts +921 -1208
- package/src/completion-provider.ts +209 -191
- package/src/{quantity-value.ts → complex-types/quantity-value.ts} +112 -22
- package/src/complex-types/temporal.ts +1737 -0
- package/src/errors.ts +25 -3
- package/src/index.ts +17 -104
- package/src/inspect.ts +4 -4
- package/src/{boxing.ts → interpreter/boxing.ts} +1 -1
- package/src/interpreter/navigator.ts +94 -0
- package/src/interpreter/runtime-context.ts +273 -0
- package/src/interpreter.ts +435 -469
- package/src/lexer.ts +188 -210
- package/src/model-provider.ts +71 -43
- package/src/operations/abs-function.ts +1 -1
- package/src/operations/aggregate-function.ts +84 -5
- package/src/operations/all-function.ts +4 -3
- package/src/operations/allFalse-function.ts +2 -1
- package/src/operations/allTrue-function.ts +2 -1
- package/src/operations/and-operator.ts +2 -1
- package/src/operations/anyFalse-function.ts +2 -1
- package/src/operations/anyTrue-function.ts +2 -1
- package/src/operations/as-function.ts +58 -0
- package/src/operations/as-operator.ts +57 -19
- package/src/operations/ceiling-function.ts +1 -1
- package/src/operations/children-function.ts +14 -5
- package/src/operations/combine-function.ts +6 -3
- package/src/operations/combine-operator.ts +6 -7
- package/src/operations/comparison.ts +692 -0
- package/src/operations/contains-function.ts +1 -1
- package/src/operations/contains-operator.ts +2 -1
- package/src/operations/convertsToBoolean-function.ts +78 -0
- package/src/operations/convertsToDecimal-function.ts +82 -0
- package/src/operations/convertsToInteger-function.ts +71 -0
- package/src/operations/convertsToLong-function.ts +89 -0
- package/src/operations/convertsToQuantity-function.ts +116 -0
- package/src/operations/convertsToString-function.ts +88 -0
- package/src/operations/count-function.ts +2 -1
- package/src/operations/dateOf-function.ts +69 -0
- package/src/operations/dayOf-function.ts +66 -0
- package/src/operations/decimal-boundaries.ts +133 -0
- package/src/operations/defineVariable-function.ts +130 -17
- package/src/operations/distinct-function.ts +1 -1
- package/src/operations/div-operator.ts +1 -1
- package/src/operations/divide-operator.ts +12 -7
- package/src/operations/dot-operator.ts +1 -1
- package/src/operations/empty-function.ts +30 -21
- package/src/operations/endsWith-function.ts +6 -1
- package/src/operations/equal-operator.ts +23 -32
- package/src/operations/equivalent-operator.ts +13 -53
- package/src/operations/exclude-function.ts +2 -1
- package/src/operations/exists-function.ts +4 -3
- package/src/operations/first-function.ts +1 -1
- package/src/operations/floor-function.ts +1 -1
- package/src/operations/greater-operator.ts +20 -3
- package/src/operations/greater-or-equal-operator.ts +20 -3
- package/src/operations/highBoundary-function.ts +120 -0
- package/src/operations/hourOf-function.ts +66 -0
- package/src/operations/iif-function.ts +186 -7
- package/src/operations/implies-operator.ts +1 -1
- package/src/operations/in-operator.ts +2 -1
- package/src/operations/index.ts +41 -0
- package/src/operations/indexOf-function.ts +1 -1
- package/src/operations/intersect-function.ts +1 -1
- package/src/operations/is-function.ts +59 -0
- package/src/operations/is-operator.ts +20 -9
- package/src/operations/isDistinct-function.ts +2 -1
- package/src/operations/join-function.ts +1 -1
- package/src/operations/last-function.ts +1 -1
- package/src/operations/lastIndexOf-function.ts +85 -0
- package/src/operations/length-function.ts +1 -1
- package/src/operations/less-operator.ts +20 -3
- package/src/operations/less-or-equal-operator.ts +20 -3
- package/src/operations/less-than.ts +2 -2
- package/src/operations/lowBoundary-function.ts +120 -0
- package/src/operations/lower-function.ts +1 -1
- package/src/operations/matches-function.ts +86 -0
- package/src/operations/matchesFull-function.ts +96 -0
- package/src/operations/millisecondOf-function.ts +66 -0
- package/src/operations/minus-operator.ts +69 -4
- package/src/operations/minuteOf-function.ts +66 -0
- package/src/operations/mod-operator.ts +1 -1
- package/src/operations/monthOf-function.ts +66 -0
- package/src/operations/multiply-operator.ts +27 -3
- package/src/operations/not-equal-operator.ts +24 -30
- package/src/operations/not-equivalent-operator.ts +13 -53
- package/src/operations/not-function.ts +1 -1
- package/src/operations/ofType-function.ts +8 -12
- package/src/operations/or-operator.ts +2 -1
- package/src/operations/plus-operator.ts +71 -7
- package/src/operations/power-function.ts +35 -10
- package/src/operations/repeat-function.ts +169 -0
- package/src/operations/replace-function.ts +1 -1
- package/src/operations/replaceMatches-function.ts +120 -0
- package/src/operations/round-function.ts +1 -1
- package/src/operations/secondOf-function.ts +66 -0
- package/src/operations/select-function.ts +66 -5
- package/src/operations/single-function.ts +1 -1
- package/src/operations/skip-function.ts +1 -1
- package/src/operations/split-function.ts +1 -1
- package/src/operations/sqrt-function.ts +15 -8
- package/src/operations/startsWith-function.ts +1 -1
- package/src/operations/subsetOf-function.ts +6 -2
- package/src/operations/substring-function.ts +1 -1
- package/src/operations/supersetOf-function.ts +6 -2
- package/src/operations/tail-function.ts +1 -1
- package/src/operations/take-function.ts +1 -1
- package/src/operations/temporal-functions.ts +555 -0
- package/src/operations/timeOf-function.ts +67 -0
- package/src/operations/timezoneOffsetOf-function.ts +69 -0
- package/src/operations/toBoolean-function.ts +27 -8
- package/src/operations/toChars-function.ts +56 -0
- package/src/operations/toDecimal-function.ts +27 -8
- package/src/operations/toInteger-function.ts +15 -3
- package/src/operations/toLong-function.ts +98 -0
- package/src/operations/toQuantity-function.ts +181 -0
- package/src/operations/toString-function.ts +45 -3
- package/src/operations/trace-function.ts +1 -1
- package/src/operations/trim-function.ts +1 -1
- package/src/operations/truncate-function.ts +1 -1
- package/src/operations/unary-minus-operator.ts +2 -2
- package/src/operations/unary-plus-operator.ts +1 -1
- package/src/operations/union-function.ts +1 -1
- package/src/operations/union-operator.ts +16 -26
- package/src/operations/upper-function.ts +1 -1
- package/src/operations/where-function.ts +3 -3
- package/src/operations/xor-operator.ts +1 -1
- package/src/operations/yearOf-function.ts +66 -0
- package/src/{cursor-nodes.ts → parser/cursor-nodes.ts} +10 -7
- package/src/parser.ts +248 -501
- package/src/registry.ts +53 -42
- package/src/types.ts +128 -16
- package/src/utils/pprint.ts +151 -0
package/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,1001 @@ 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
|
-
const
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
const func = registry.getFunction(funcName);
|
|
193
|
-
if (func && func.signatures && func.signatures.length > 0 && node.left.typeInfo) {
|
|
194
|
-
// Check if any signature matches the input type
|
|
195
|
-
let matchFound = false;
|
|
196
|
-
let expectedTypes: string[] = [];
|
|
197
|
-
|
|
198
|
-
for (const signature of func.signatures) {
|
|
199
|
-
if (signature.input) {
|
|
200
|
-
if (this.isTypeCompatible(node.left.typeInfo, signature.input)) {
|
|
201
|
-
matchFound = true;
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
expectedTypes.push(this.typeToString(signature.input));
|
|
205
|
-
} else {
|
|
206
|
-
// If any signature has no input constraint, it matches
|
|
207
|
-
matchFound = true;
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (!matchFound) {
|
|
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
|
+
const leftResult = await this.analyzeNode(node.left, context);
|
|
202
|
+
if (this.stoppedAtCursor) {
|
|
203
|
+
return { type: { type: 'Any', singleton: false }, diagnostics: leftResult.diagnostics };
|
|
241
204
|
}
|
|
205
|
+
|
|
206
|
+
// Right side gets left's output as input, with any context changes
|
|
207
|
+
const rightContext = (leftResult.context || context).withInputType(leftResult.type);
|
|
208
|
+
const rightResult = await this.analyzeNode(node.right, rightContext);
|
|
209
|
+
|
|
210
|
+
diagnostics.push(...leftResult.diagnostics, ...rightResult.diagnostics);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
type: rightResult.type,
|
|
214
|
+
diagnostics,
|
|
215
|
+
context: rightResult.context // Propagate context changes (for defineVariable)
|
|
216
|
+
};
|
|
242
217
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return;
|
|
218
|
+
|
|
219
|
+
// Handle other binary operators
|
|
220
|
+
const leftResult = await this.analyzeNode(node.left, context);
|
|
221
|
+
if (this.stoppedAtCursor) {
|
|
222
|
+
return { type: { type: 'Any', singleton: false }, diagnostics: leftResult.diagnostics };
|
|
249
223
|
}
|
|
250
224
|
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
this.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
225
|
+
// Check if right side is a cursor node - if so, set proper context
|
|
226
|
+
if (this.cursorMode && isCursorNode(node.right)) {
|
|
227
|
+
this.stoppedAtCursor = true;
|
|
228
|
+
this.cursorContext = {
|
|
229
|
+
cursorNode: node.right as AnyCursorNode,
|
|
230
|
+
typeBeforeCursor: leftResult.type,
|
|
231
|
+
expectedType: undefined
|
|
232
|
+
};
|
|
233
|
+
return {
|
|
234
|
+
type: { type: 'Any', singleton: false },
|
|
235
|
+
diagnostics: leftResult.diagnostics
|
|
236
|
+
};
|
|
257
237
|
}
|
|
258
238
|
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
239
|
+
// For most operators, right side evaluates with original context (not left's output)
|
|
240
|
+
const rightResult = await this.analyzeNode(node.right, context);
|
|
241
|
+
|
|
242
|
+
diagnostics.push(...leftResult.diagnostics, ...rightResult.diagnostics);
|
|
243
|
+
|
|
244
|
+
// Get operator definition for type checking
|
|
245
|
+
const operatorDef = registry.getOperatorDefinition(node.operator);
|
|
246
|
+
if (!operatorDef) {
|
|
247
|
+
diagnostics.push(toDiagnostic(Errors.unknownOperator(node.operator, node.range)));
|
|
248
|
+
return {
|
|
249
|
+
type: { type: 'Any', singleton: false },
|
|
250
|
+
diagnostics
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Check operator signatures for type compatibility
|
|
255
|
+
if (operatorDef.signatures && operatorDef.signatures.length > 0) {
|
|
256
|
+
const matchingSignature = matchOperatorSignature(leftResult.type, rightResult.type, operatorDef) || null;
|
|
257
|
+
if (!matchingSignature) {
|
|
258
|
+
// No matching signature found - report type error
|
|
259
|
+
// But don't report if either side is Any (could be from an error)
|
|
260
|
+
if (leftResult.type.type !== 'Any' && rightResult.type.type !== 'Any') {
|
|
261
|
+
const leftTypeStr = leftResult.type.singleton ? leftResult.type.type : `${leftResult.type.type}[]`;
|
|
262
|
+
const rightTypeStr = rightResult.type.singleton ? rightResult.type.type : `${rightResult.type.type}[]`;
|
|
263
|
+
diagnostics.push(this.createError(
|
|
264
|
+
node,
|
|
265
|
+
`Operator '${node.operator}' cannot be applied to types ${leftTypeStr} and ${rightTypeStr}`,
|
|
266
|
+
ErrorCodes.OPERATOR_TYPE_MISMATCH
|
|
267
|
+
));
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
type: { type: 'Any', singleton: false },
|
|
271
|
+
diagnostics
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Determine result type from matching signature
|
|
276
|
+
const resultType = resolveResultType(matchingSignature.result as any, {
|
|
277
|
+
input: context.inputType,
|
|
278
|
+
left: leftResult.type,
|
|
279
|
+
right: rightResult.type,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
type: resultType,
|
|
284
|
+
diagnostics
|
|
285
|
+
};
|
|
262
286
|
}
|
|
287
|
+
|
|
288
|
+
// If no signatures defined, return Any type
|
|
289
|
+
return {
|
|
290
|
+
type: { type: 'Any', singleton: false },
|
|
291
|
+
diagnostics
|
|
292
|
+
};
|
|
263
293
|
}
|
|
264
294
|
|
|
265
|
-
|
|
266
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Analyzes function calls, delegating to function's analyze method if available.
|
|
297
|
+
*/
|
|
298
|
+
private async analyzeFunction(node: FunctionNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
|
|
299
|
+
const diagnostics: Diagnostic[] = [];
|
|
300
|
+
|
|
301
|
+
const functionName = this.getFunctionName(node);
|
|
302
|
+
if (!functionName) {
|
|
303
|
+
diagnostics.push(this.createError(node.name, 'Invalid function name', ErrorCodes.INVALID_SYNTAX));
|
|
304
|
+
return { type: { type: 'Any', singleton: false }, diagnostics };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const funcDef = registry.getFunction(functionName);
|
|
308
|
+
if (!funcDef) {
|
|
309
|
+
diagnostics.push(toDiagnostic(Errors.unknownFunction(functionName, node.range)));
|
|
310
|
+
return { type: { type: 'Any', singleton: false }, diagnostics };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const arity = this.validateArity(funcDef, node, functionName);
|
|
314
|
+
diagnostics.push(...arity.diagnostics);
|
|
315
|
+
|
|
316
|
+
// Early union rules for ofType/is/as
|
|
317
|
+
diagnostics.push(...this.validateUnionTypeFilters(functionName, node, context));
|
|
318
|
+
|
|
319
|
+
// Custom analyze
|
|
320
|
+
if (funcDef.analyze) {
|
|
321
|
+
const result = funcDef.analyze(context, node.arguments);
|
|
322
|
+
const analysisResult = result instanceof Promise ? await result : result;
|
|
323
|
+
return {
|
|
324
|
+
...analysisResult,
|
|
325
|
+
diagnostics: [...diagnostics, ...analysisResult.diagnostics]
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Default path: analyze args
|
|
330
|
+
const argAnalysis = await this.analyzeArguments(funcDef, node, context, functionName);
|
|
331
|
+
diagnostics.push(...argAnalysis.diagnostics);
|
|
332
|
+
if (this.stoppedAtCursor) {
|
|
333
|
+
return { type: { type: 'Any', singleton: false }, diagnostics };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Signature matching and diagnostics
|
|
337
|
+
const signatureResult = this.matchAndDiagnoseSignature(
|
|
338
|
+
funcDef,
|
|
339
|
+
context.inputType,
|
|
340
|
+
argAnalysis.argTypes,
|
|
341
|
+
node,
|
|
342
|
+
functionName,
|
|
343
|
+
arity.hasError
|
|
344
|
+
);
|
|
345
|
+
diagnostics.push(...signatureResult.diagnostics);
|
|
346
|
+
if (signatureResult.earlyReturn) {
|
|
347
|
+
return { type: signatureResult.earlyReturn, diagnostics };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Empty propagation
|
|
351
|
+
if (this.propagatesEmpty(funcDef, context.inputType, argAnalysis.argTypes)) {
|
|
352
|
+
return {
|
|
353
|
+
type: { type: 'Any', singleton: false, isEmpty: true },
|
|
354
|
+
diagnostics,
|
|
355
|
+
context
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Result inference
|
|
360
|
+
let resultType = await this.inferFunctionResultType(
|
|
361
|
+
funcDef,
|
|
362
|
+
node,
|
|
363
|
+
context,
|
|
364
|
+
argAnalysis.argTypes,
|
|
365
|
+
signatureResult.match
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
if (functionName === 'where') {
|
|
369
|
+
resultType = { ...resultType, singleton: false };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { type: resultType, diagnostics, context };
|
|
267
373
|
}
|
|
268
374
|
|
|
269
|
-
private
|
|
375
|
+
private getFunctionName(node: FunctionNode): string | null {
|
|
270
376
|
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
|
-
|
|
377
|
+
return (node.name as IdentifierNode).name;
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private validateArity(
|
|
383
|
+
funcDef: FunctionDefinition,
|
|
384
|
+
node: FunctionNode,
|
|
385
|
+
functionName: string
|
|
386
|
+
): { diagnostics: Diagnostic[]; hasError: boolean } {
|
|
387
|
+
const diagnostics: Diagnostic[] = [];
|
|
388
|
+
let hasError = false;
|
|
389
|
+
if (funcDef.signatures && funcDef.signatures.length > 0) {
|
|
390
|
+
const signature = funcDef.signatures[0];
|
|
391
|
+
if (signature) {
|
|
392
|
+
const params = signature.parameters || [];
|
|
393
|
+
const requiredCount = params.filter(p => !p.optional).length;
|
|
394
|
+
const maxCount = params.length;
|
|
395
|
+
const actualCount = node.arguments.length;
|
|
396
|
+
|
|
397
|
+
if (actualCount < requiredCount) {
|
|
398
|
+
diagnostics.push(
|
|
399
|
+
this.createError(
|
|
400
|
+
node,
|
|
401
|
+
`${functionName} expects at least ${requiredCount} argument${requiredCount !== 1 ? 's' : ''}, got ${actualCount}`,
|
|
402
|
+
ErrorCodes.WRONG_ARGUMENT_COUNT
|
|
403
|
+
)
|
|
291
404
|
);
|
|
405
|
+
hasError = true;
|
|
406
|
+
} else if (actualCount > maxCount) {
|
|
407
|
+
diagnostics.push(
|
|
408
|
+
this.createError(
|
|
409
|
+
node,
|
|
410
|
+
`${functionName} expects at most ${maxCount} argument${maxCount !== 1 ? 's' : ''}, got ${actualCount}`,
|
|
411
|
+
ErrorCodes.WRONG_ARGUMENT_COUNT
|
|
412
|
+
)
|
|
413
|
+
);
|
|
414
|
+
hasError = true;
|
|
292
415
|
}
|
|
293
416
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
417
|
+
}
|
|
418
|
+
return { diagnostics, hasError };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private validateUnionTypeFilters(
|
|
422
|
+
functionName: string,
|
|
423
|
+
node: FunctionNode,
|
|
424
|
+
context: AnalysisContext
|
|
425
|
+
): Diagnostic[] {
|
|
426
|
+
const diagnostics: Diagnostic[] = [];
|
|
427
|
+
if (!['ofType', 'is', 'as'].includes(functionName) || node.arguments.length === 0) {
|
|
428
|
+
return diagnostics;
|
|
429
|
+
}
|
|
430
|
+
const inputType = context.inputType;
|
|
431
|
+
if (!isUnionType(inputType)) {
|
|
432
|
+
return diagnostics;
|
|
433
|
+
}
|
|
434
|
+
const typeArg = node.arguments[0]!;
|
|
435
|
+
let targetType: string | undefined;
|
|
436
|
+
if (typeArg.type === NodeType.Identifier) {
|
|
437
|
+
targetType = (typeArg as IdentifierNode).name;
|
|
438
|
+
}
|
|
439
|
+
if (!targetType) {
|
|
440
|
+
return diagnostics;
|
|
441
|
+
}
|
|
442
|
+
const diag = validateUnionChoice(inputType, targetType, typeArg.range || node.range, 'invalid-type-filter', 'Type');
|
|
443
|
+
if (diag && functionName === 'ofType') diagnostics.push(diag);
|
|
444
|
+
return diagnostics;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private async analyzeArguments(
|
|
448
|
+
funcDef: FunctionDefinition,
|
|
449
|
+
node: FunctionNode,
|
|
450
|
+
context: AnalysisContext,
|
|
451
|
+
functionName: string
|
|
452
|
+
): Promise<{ argTypes: TypeInfo[]; diagnostics: Diagnostic[] }> {
|
|
453
|
+
const diagnostics: Diagnostic[] = [];
|
|
454
|
+
const argTypes: TypeInfo[] = [];
|
|
455
|
+
const signature = funcDef.signatures?.[0];
|
|
456
|
+
const params = signature?.parameters || [];
|
|
457
|
+
|
|
458
|
+
for (let i = 0; i < node.arguments.length; i++) {
|
|
459
|
+
const arg = node.arguments[i]!;
|
|
460
|
+
const param = params[i];
|
|
461
|
+
const isTypeParameter = !!param?.typeReference;
|
|
462
|
+
|
|
463
|
+
if (isTypeParameter) {
|
|
464
|
+
argTypes.push({ type: 'TypeReference' as TypeName, singleton: true });
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (param?.expression) {
|
|
469
|
+
const itemType = { ...context.inputType, singleton: true };
|
|
470
|
+
const exprContext = context
|
|
471
|
+
.withSystemVariable('$this', itemType)
|
|
472
|
+
.withSystemVariable('$index', { type: 'Integer', singleton: true });
|
|
473
|
+
const argResult = await this.analyzeNode(arg, exprContext);
|
|
474
|
+
diagnostics.push(...argResult.diagnostics);
|
|
475
|
+
argTypes.push(argResult.type);
|
|
476
|
+
if (this.stoppedAtCursor) {
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const thisType = context.systemVariables.get('$this') || context.inputType;
|
|
483
|
+
const argContext = context.withInputType(thisType);
|
|
484
|
+
const argResult = await this.analyzeNode(arg, argContext);
|
|
485
|
+
diagnostics.push(...argResult.diagnostics);
|
|
486
|
+
argTypes.push(argResult.type);
|
|
487
|
+
if (this.stoppedAtCursor) {
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return { argTypes, diagnostics };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private matchAndDiagnoseSignature(
|
|
496
|
+
funcDef: FunctionDefinition,
|
|
497
|
+
actualInput: TypeInfo,
|
|
498
|
+
argTypes: TypeInfo[],
|
|
499
|
+
node: FunctionNode,
|
|
500
|
+
functionName: string,
|
|
501
|
+
hasArityError: boolean
|
|
502
|
+
): { match: FunctionSignature | null; diagnostics: Diagnostic[]; earlyReturn?: TypeInfo } {
|
|
503
|
+
const diagnostics: Diagnostic[] = [];
|
|
504
|
+
let match: FunctionSignature | null = null;
|
|
505
|
+
|
|
506
|
+
if (!hasArityError && funcDef.signatures && funcDef.signatures.length > 0) {
|
|
507
|
+
match = matchFunctionSignature(actualInput, argTypes, funcDef) || null;
|
|
508
|
+
|
|
509
|
+
if (!match) {
|
|
510
|
+
const inputIsEmpty = isEmptyCollection(actualInput);
|
|
511
|
+
if (inputIsEmpty && !funcDef.doesNotPropagateEmpty) {
|
|
512
|
+
const sig = funcDef.signatures[0];
|
|
513
|
+
if (sig) {
|
|
514
|
+
diagnostics.push(
|
|
515
|
+
...checkParamTypes(sig, argTypes, node.arguments, {
|
|
516
|
+
warnOnSingletonOnly: false,
|
|
517
|
+
doesNotPropagateEmpty: !!funcDef.doesNotPropagateEmpty,
|
|
518
|
+
treatEmptyAsWarning: true,
|
|
519
|
+
errorCode: ErrorCodes.ARGUMENT_TYPE_MISMATCH,
|
|
520
|
+
})
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
let inputMatchingSignature: FunctionSignature | null = null;
|
|
525
|
+
for (const sig of funcDef.signatures) {
|
|
526
|
+
let inputMatches = true;
|
|
527
|
+
if (sig.input) {
|
|
528
|
+
const expectedInput = sig.input;
|
|
529
|
+
const singletonMatch = !expectedInput.singleton || actualInput.singleton === true;
|
|
530
|
+
const typeMatch =
|
|
531
|
+
expectedInput.type === 'Any' ||
|
|
532
|
+
actualInput.type === 'Any' ||
|
|
533
|
+
expectedInput.type === actualInput.type ||
|
|
534
|
+
(expectedInput.type === 'Decimal' && actualInput.type === 'Integer');
|
|
535
|
+
inputMatches = singletonMatch && typeMatch;
|
|
536
|
+
}
|
|
537
|
+
if (inputMatches) {
|
|
538
|
+
inputMatchingSignature = sig;
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
312
541
|
}
|
|
313
|
-
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
|
|
542
|
+
|
|
543
|
+
if (inputMatchingSignature && inputMatchingSignature.parameters) {
|
|
544
|
+
diagnostics.push(
|
|
545
|
+
...checkParamTypes(inputMatchingSignature, argTypes, node.arguments, {
|
|
546
|
+
warnOnSingletonOnly: true,
|
|
547
|
+
doesNotPropagateEmpty: !!funcDef.doesNotPropagateEmpty,
|
|
548
|
+
errorCode: ErrorCodes.ARGUMENT_TYPE_MISMATCH,
|
|
549
|
+
})
|
|
317
550
|
);
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
551
|
+
} else {
|
|
552
|
+
const actualTypeStr = actualInput.singleton ? actualInput.type : `${actualInput.type}[]`;
|
|
553
|
+
const hasSingletonSignature = funcDef.signatures.some(sig => sig.input?.singleton && sig.input.type === actualInput.type);
|
|
554
|
+
const permissive = ['anyFalse', 'anyTrue'];
|
|
555
|
+
if (hasSingletonSignature && !actualInput.singleton) {
|
|
556
|
+
diagnostics.push(
|
|
557
|
+
this.createError(
|
|
558
|
+
node,
|
|
559
|
+
`${functionName} expects a singleton value, but received collection type ${actualTypeStr}`,
|
|
560
|
+
ErrorCodes.SINGLETON_REQUIRED
|
|
561
|
+
)
|
|
562
|
+
);
|
|
563
|
+
} else if (!permissive.includes(functionName)) {
|
|
564
|
+
const expectedTypes = funcDef.signatures
|
|
565
|
+
.map(sig => (sig.input ? (sig.input.singleton ? sig.input.type : `${sig.input.type}[]`) : 'Any'))
|
|
566
|
+
.filter((v, i, a) => a.indexOf(v) === i)
|
|
567
|
+
.join(' or ');
|
|
568
|
+
diagnostics.push(
|
|
569
|
+
this.createError(
|
|
570
|
+
node,
|
|
571
|
+
`Cannot apply ${functionName}() to ${actualTypeStr}. Function expects ${expectedTypes}.`,
|
|
572
|
+
ErrorCodes.INVALID_OPERAND_TYPE
|
|
573
|
+
)
|
|
574
|
+
);
|
|
328
575
|
}
|
|
329
576
|
}
|
|
330
577
|
}
|
|
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
578
|
} else {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
toDiagnostic(Errors.wrongArgumentCount(funcName, requiredParams, node.arguments.length, node.range))
|
|
579
|
+
if (match.parameters) {
|
|
580
|
+
diagnostics.push(
|
|
581
|
+
...checkParamTypes(match, argTypes, node.arguments, {
|
|
582
|
+
warnOnSingletonOnly: true,
|
|
583
|
+
doesNotPropagateEmpty: !!funcDef.doesNotPropagateEmpty,
|
|
584
|
+
errorCode: ErrorCodes.ARGUMENT_TYPE_MISMATCH,
|
|
585
|
+
})
|
|
348
586
|
);
|
|
349
|
-
} else
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
// Type check arguments if we have type information
|
|
356
|
-
if (node.typeInfo || node.arguments.some(arg => arg.typeInfo)) {
|
|
357
|
-
this.checkFunctionArgumentTypes(node, func);
|
|
587
|
+
} else {
|
|
588
|
+
const permissive = ['anyFalse', 'anyTrue'];
|
|
589
|
+
if (permissive.includes(functionName)) {
|
|
590
|
+
return { match, diagnostics, earlyReturn: { type: 'Boolean', singleton: true } };
|
|
591
|
+
}
|
|
358
592
|
}
|
|
359
593
|
}
|
|
360
594
|
}
|
|
361
|
-
|
|
362
|
-
|
|
595
|
+
|
|
596
|
+
return { match, diagnostics };
|
|
363
597
|
}
|
|
364
598
|
|
|
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);
|
|
599
|
+
private propagatesEmpty(
|
|
600
|
+
funcDef: FunctionDefinition,
|
|
601
|
+
inputType: TypeInfo,
|
|
602
|
+
argTypes: TypeInfo[]
|
|
603
|
+
): boolean {
|
|
604
|
+
if (funcDef.doesNotPropagateEmpty) {
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
607
|
+
const inputIsEmpty = isEmptyCollection(inputType);
|
|
608
|
+
const hasEmptyArgument = argTypes.some(argType => isEmptyCollection(argType));
|
|
609
|
+
return inputIsEmpty || hasEmptyArgument;
|
|
404
610
|
}
|
|
405
611
|
|
|
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
|
-
}
|
|
612
|
+
private async inferFunctionResultType(
|
|
613
|
+
funcDef: FunctionDefinition,
|
|
614
|
+
node: FunctionNode,
|
|
615
|
+
context: AnalysisContext,
|
|
616
|
+
argTypes: TypeInfo[],
|
|
617
|
+
matchingSignature: FunctionSignature | null
|
|
618
|
+
): Promise<TypeInfo> {
|
|
619
|
+
if (funcDef.inferResultType) {
|
|
620
|
+
return funcDef.inferResultType(this, node, context.inputType);
|
|
621
|
+
}
|
|
622
|
+
if (matchingSignature) {
|
|
623
|
+
return resolveResultType(matchingSignature.result as any, {
|
|
624
|
+
input: context.inputType,
|
|
625
|
+
firstParam: argTypes[0],
|
|
626
|
+
});
|
|
442
627
|
}
|
|
443
|
-
|
|
444
|
-
this.visitNode(node.expression);
|
|
628
|
+
return context.inputType;
|
|
445
629
|
}
|
|
446
630
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
631
|
+
/**
|
|
632
|
+
* Analyzes variable references, checking against context.
|
|
633
|
+
*/
|
|
634
|
+
private analyzeVariable(node: VariableNode, context: AnalysisContext): InternalAnalysisResult {
|
|
635
|
+
const varName = node.name;
|
|
636
|
+
const diagnostics: Diagnostic[] = [];
|
|
637
|
+
|
|
638
|
+
// Check if it's a user variable (starts with %)
|
|
639
|
+
if (varName.startsWith('%')) {
|
|
640
|
+
// Special handling for %context - it's a built-in environment variable
|
|
641
|
+
// that always returns the original input to the evaluation engine
|
|
642
|
+
if (varName === '%context') {
|
|
643
|
+
// %context returns the root input type (the original input to evaluate())
|
|
644
|
+
// In the analyzer, we track this as the initial input type
|
|
645
|
+
return { type: context.inputType, diagnostics, context };
|
|
461
646
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
vars.add(nameArg.value as string);
|
|
647
|
+
|
|
648
|
+
const name = varName.substring(1); // Remove % prefix
|
|
649
|
+
const varType = context.userVariables.get(name);
|
|
650
|
+
|
|
651
|
+
if (!varType) {
|
|
652
|
+
// If we have dynamic variables in scope, we can't be sure this is an error
|
|
653
|
+
if (context.hasDynamicVariables) {
|
|
654
|
+
diagnostics.push(this.createWarning(node, `Variable '${varName}' may not be defined (dynamic variables in scope)`));
|
|
655
|
+
// Return Any type since we don't know the actual type
|
|
656
|
+
return { type: { type: 'Any', singleton: false }, diagnostics };
|
|
657
|
+
} else {
|
|
658
|
+
// No dynamic variables, so this is definitely an error
|
|
659
|
+
diagnostics.push(this.createError(node, Errors.unknownUserVariable(varName).message, ErrorCodes.UNKNOWN_USER_VARIABLE));
|
|
660
|
+
return { type: { type: 'Any', singleton: false }, diagnostics };
|
|
477
661
|
}
|
|
478
662
|
}
|
|
663
|
+
|
|
664
|
+
// Attach type info to the node for backward compatibility
|
|
665
|
+
node.typeInfo = varType;
|
|
666
|
+
return { type: varType, diagnostics, context };
|
|
479
667
|
}
|
|
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
|
-
}
|
|
668
|
+
|
|
669
|
+
// Check system variables
|
|
670
|
+
const sysVarType = context.systemVariables.get(varName);
|
|
671
|
+
if (sysVarType) {
|
|
672
|
+
// Attach type info to the node for backward compatibility
|
|
673
|
+
node.typeInfo = sysVarType;
|
|
674
|
+
return { type: sysVarType, diagnostics, context };
|
|
502
675
|
}
|
|
503
|
-
|
|
504
|
-
|
|
676
|
+
|
|
677
|
+
// Unknown variable
|
|
678
|
+
diagnostics.push(this.createError(node, `Unknown variable: ${varName}`, ErrorCodes.UNKNOWN_VARIABLE));
|
|
679
|
+
return { type: { type: 'Any', singleton: false }, diagnostics };
|
|
505
680
|
}
|
|
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
|
-
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Analyzes identifier nodes (property access).
|
|
684
|
+
*/
|
|
685
|
+
private async analyzeIdentifier(node: IdentifierNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
|
|
686
|
+
const name = 'name' in node ? node.name : '';
|
|
687
|
+
const diagnostics: Diagnostic[] = [];
|
|
688
|
+
|
|
689
|
+
// Try to use model provider for accurate type information
|
|
690
|
+
if (context.modelProvider) {
|
|
691
|
+
// First try to navigate from input type (property access)
|
|
692
|
+
const elementType = await context.modelProvider.getElementType(context.inputType, name);
|
|
693
|
+
if (elementType) {
|
|
694
|
+
return {
|
|
695
|
+
type: elementType,
|
|
696
|
+
diagnostics,
|
|
697
|
+
context
|
|
698
|
+
};
|
|
533
699
|
}
|
|
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
|
-
}
|
|
700
|
+
// Chain-head rule: at the head of a navigation chain, allow treating the
|
|
701
|
+
// identifier as a known type to seed the chain (e.g., Patient.name)
|
|
702
|
+
if ((context as any)._chainHead === true) {
|
|
703
|
+
const typeInfo = await context.modelProvider.getType(name);
|
|
704
|
+
if (typeInfo) {
|
|
705
|
+
return {
|
|
706
|
+
type: typeInfo,
|
|
707
|
+
diagnostics,
|
|
708
|
+
context
|
|
709
|
+
};
|
|
566
710
|
}
|
|
567
711
|
}
|
|
712
|
+
|
|
713
|
+
// If property not found and we have a concrete non-union type, report warning
|
|
714
|
+
// FHIRPath returns empty for unknown properties, not an error
|
|
715
|
+
const mc: any = context.inputType.modelContext;
|
|
716
|
+
const isUnion = !!(mc && typeof mc === 'object' && 'isUnion' in mc && mc.isUnion);
|
|
717
|
+
if (context.inputType.namespace && context.inputType.name && context.inputType.modelContext && !isUnion) {
|
|
718
|
+
const typeStr = `${context.inputType.namespace}.${context.inputType.name}`;
|
|
719
|
+
diagnostics.push(toDiagnostic(Errors.unknownProperty(name, typeStr, node.range), DiagnosticSeverity.Warning));
|
|
720
|
+
return {
|
|
721
|
+
type: { type: 'Any', singleton: false },
|
|
722
|
+
diagnostics,
|
|
723
|
+
context
|
|
724
|
+
};
|
|
725
|
+
}
|
|
568
726
|
}
|
|
569
727
|
|
|
570
|
-
|
|
728
|
+
// Without a model provider, we can't know the type
|
|
729
|
+
// Return Any type - don't make assumptions
|
|
730
|
+
return {
|
|
731
|
+
type: { type: 'Any', singleton: false },
|
|
732
|
+
diagnostics,
|
|
733
|
+
context
|
|
734
|
+
};
|
|
571
735
|
}
|
|
572
736
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
return this.inferErrorNodeType(node as ErrorNode, inputType);
|
|
579
|
-
}
|
|
737
|
+
/**
|
|
738
|
+
* Analyzes literal values.
|
|
739
|
+
*/
|
|
740
|
+
private analyzeLiteral(node: LiteralNode, context: AnalysisContext): InternalAnalysisResult {
|
|
741
|
+
let type: TypeInfo;
|
|
580
742
|
|
|
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
743
|
switch (node.valueType) {
|
|
625
744
|
case 'string':
|
|
626
|
-
|
|
745
|
+
type = { type: 'String', singleton: true };
|
|
746
|
+
break;
|
|
627
747
|
case 'number':
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
type:
|
|
631
|
-
singleton: true
|
|
632
|
-
|
|
748
|
+
// Check if integer or decimal
|
|
749
|
+
type = Number.isInteger(node.value)
|
|
750
|
+
? { type: 'Integer', singleton: true }
|
|
751
|
+
: { type: 'Decimal', singleton: true };
|
|
752
|
+
break;
|
|
633
753
|
case 'boolean':
|
|
634
|
-
|
|
754
|
+
type = { type: 'Boolean', singleton: true };
|
|
755
|
+
break;
|
|
635
756
|
case 'date':
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
return { type: 'DateTime', singleton: true };
|
|
757
|
+
type = { type: 'Date', singleton: true };
|
|
758
|
+
break;
|
|
639
759
|
case 'time':
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
760
|
+
type = { type: 'Time', singleton: true };
|
|
761
|
+
break;
|
|
762
|
+
case 'datetime':
|
|
763
|
+
type = { type: 'DateTime', singleton: true };
|
|
764
|
+
break;
|
|
643
765
|
default:
|
|
644
|
-
|
|
766
|
+
type = { type: 'Any', singleton: true };
|
|
645
767
|
}
|
|
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
768
|
|
|
663
|
-
|
|
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
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// Default to first signature's result type
|
|
672
|
-
const defaultResult = operator.signatures[0]?.result || { type: 'Any', singleton: false };
|
|
673
|
-
return this.resolveResultType(defaultResult, inputType, leftType, rightType);
|
|
769
|
+
return { type, diagnostics: [] };
|
|
674
770
|
}
|
|
675
771
|
|
|
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
|
-
}
|
|
772
|
+
private analyzeTemporalLiteral(node: TemporalLiteralNode, context: AnalysisContext): InternalAnalysisResult {
|
|
773
|
+
let type: TypeInfo;
|
|
683
774
|
|
|
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
|
-
}
|
|
775
|
+
switch (node.valueType) {
|
|
776
|
+
case 'date':
|
|
777
|
+
type = { type: 'Date', singleton: true };
|
|
778
|
+
break;
|
|
779
|
+
case 'time':
|
|
780
|
+
type = { type: 'Time', singleton: true };
|
|
781
|
+
break;
|
|
782
|
+
case 'datetime':
|
|
783
|
+
type = { type: 'DateTime', singleton: true };
|
|
784
|
+
break;
|
|
785
|
+
default:
|
|
786
|
+
type = { type: 'Any', singleton: true };
|
|
702
787
|
}
|
|
703
788
|
|
|
704
|
-
|
|
705
|
-
return { type: 'Any', singleton: false };
|
|
789
|
+
return { type, diagnostics: [] };
|
|
706
790
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
if (
|
|
717
|
-
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Analyzes unary operators.
|
|
794
|
+
*/
|
|
795
|
+
private async analyzeUnary(node: UnaryNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
|
|
796
|
+
const operandResult = await this.analyzeNode(node.operand, context);
|
|
797
|
+
const diagnostics: Diagnostic[] = [...operandResult.diagnostics];
|
|
798
|
+
const opDef = registry.getOperatorDefinition(node.operator);
|
|
799
|
+
|
|
800
|
+
if (opDef && opDef.signatures && opDef.signatures.length > 0) {
|
|
801
|
+
// Use first signature's result if defined
|
|
802
|
+
const sig = opDef.signatures[0]!;
|
|
803
|
+
const resultType = typeof sig.result === 'object' ? sig.result : operandResult.type;
|
|
804
|
+
return { type: resultType, diagnostics };
|
|
718
805
|
}
|
|
719
|
-
|
|
720
|
-
|
|
806
|
+
|
|
807
|
+
// Fallback: preserve operand type
|
|
808
|
+
return { type: operandResult.type, diagnostics };
|
|
721
809
|
}
|
|
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 };
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Analyzes index operations.
|
|
813
|
+
*/
|
|
814
|
+
private async analyzeIndex(node: IndexNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
|
|
815
|
+
const exprResult = await this.analyzeNode(node.expression, context);
|
|
816
|
+
const indexResult = await this.analyzeNode(node.index, context);
|
|
817
|
+
const diagnostics: Diagnostic[] = [...exprResult.diagnostics, ...indexResult.diagnostics];
|
|
818
|
+
|
|
819
|
+
// Index should be Integer singleton; if unknown, skip strict error
|
|
820
|
+
const idxType = indexResult.type;
|
|
821
|
+
const isIntegerSingleton = idxType.type === 'Integer' && idxType.singleton === true;
|
|
822
|
+
if (!isIntegerSingleton && idxType.type !== 'Any') {
|
|
823
|
+
diagnostics.push(this.createError(node.index, 'Index must be an Integer singleton', ErrorCodes.ARGUMENT_TYPE_MISMATCH));
|
|
771
824
|
}
|
|
772
|
-
|
|
773
|
-
//
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
825
|
+
|
|
826
|
+
// Result is the element type (singleton) of the expression collection
|
|
827
|
+
const exprType = exprResult.type;
|
|
828
|
+
const resultType = { ...exprType, singleton: true };
|
|
829
|
+
return { type: resultType, diagnostics };
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Analyzes quantity literals.
|
|
834
|
+
*/
|
|
835
|
+
private analyzeQuantity(node: QuantityNode, context: AnalysisContext): InternalAnalysisResult {
|
|
836
|
+
return {
|
|
837
|
+
type: { type: 'Quantity', singleton: true },
|
|
838
|
+
diagnostics: [],
|
|
839
|
+
context
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Analyzes collection literals.
|
|
845
|
+
*/
|
|
846
|
+
private async analyzeCollection(node: CollectionNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
|
|
847
|
+
const diagnostics: Diagnostic[] = [];
|
|
848
|
+
const isEmpty = node.elements.length === 0;
|
|
849
|
+
const elementTypes: TypeInfo[] = [];
|
|
850
|
+
|
|
851
|
+
for (const element of node.elements) {
|
|
852
|
+
const elemResult = await this.analyzeNode(element, context);
|
|
853
|
+
diagnostics.push(...elemResult.diagnostics);
|
|
854
|
+
elementTypes.push(elemResult.type);
|
|
855
|
+
if (this.stoppedAtCursor) {
|
|
856
|
+
return { type: { type: 'Any', singleton: false }, diagnostics };
|
|
787
857
|
}
|
|
788
|
-
// No arguments at all
|
|
789
|
-
return { type: 'Any', singleton: false };
|
|
790
858
|
}
|
|
791
|
-
|
|
792
|
-
//
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
859
|
+
|
|
860
|
+
// Infer common element type
|
|
861
|
+
let elementType: TypeInfo = { type: 'Any', singleton: true };
|
|
862
|
+
if (!isEmpty) {
|
|
863
|
+
elementType = elementTypes[0]!;
|
|
864
|
+
for (let i = 1; i < elementTypes.length; i++) {
|
|
865
|
+
const t = elementTypes[i]!;
|
|
866
|
+
if (elementType.type === t.type) {
|
|
867
|
+
// keep; if any is non-singleton, result stays collection anyway
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
// Promote Integer to Decimal when mixed
|
|
871
|
+
if ((elementType.type === 'Integer' && t.type === 'Decimal') || (elementType.type === 'Decimal' && t.type === 'Integer')) {
|
|
872
|
+
elementType = { type: 'Decimal', singleton: true };
|
|
873
|
+
continue;
|
|
798
874
|
}
|
|
875
|
+
// Unknown/heterogeneous → Any
|
|
876
|
+
elementType = { type: 'Any', singleton: true };
|
|
877
|
+
break;
|
|
799
878
|
}
|
|
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
879
|
}
|
|
819
|
-
|
|
820
|
-
|
|
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;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
return { type: 'Any', singleton: false };
|
|
880
|
+
|
|
881
|
+
return { type: { ...elementType, singleton: false, isEmpty }, diagnostics };
|
|
835
882
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* Analyzes membership test (is operator).
|
|
886
|
+
*/
|
|
887
|
+
private async analyzeMembershipTest(node: MembershipTestNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
|
|
888
|
+
const exprResult = await this.analyzeNode(node.expression, context);
|
|
889
|
+
const diagnostics = [...exprResult.diagnostics];
|
|
890
|
+
|
|
891
|
+
// ModelProvider requirement for non-primitive target types
|
|
892
|
+
const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
|
|
893
|
+
if (!context.modelProvider && !primitiveTypes.includes(node.targetType)) {
|
|
894
|
+
diagnostics.push(toDiagnostic(Errors.modelProviderRequired('is', node.range)));
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Check if testing against a union type
|
|
898
|
+
if (isUnionType(exprResult.type)) {
|
|
899
|
+
const targetType = node.targetType;
|
|
900
|
+
const choices = getUnionChoices(exprResult.type);
|
|
901
|
+
if (choices.length > 0 && !choices.includes(targetType)) {
|
|
902
|
+
diagnostics.push({
|
|
903
|
+
severity: DiagnosticSeverity.Warning,
|
|
904
|
+
code: 'invalid-type-test',
|
|
905
|
+
message: `Type test 'is ${targetType}' will always be false - type not present in union. Available types: ${choices.join(', ')}`,
|
|
906
|
+
range: node.range
|
|
907
|
+
});
|
|
855
908
|
}
|
|
856
909
|
}
|
|
857
910
|
|
|
858
|
-
return {
|
|
911
|
+
return {
|
|
912
|
+
type: { type: 'Boolean', singleton: true },
|
|
913
|
+
diagnostics
|
|
914
|
+
};
|
|
859
915
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Analyzes type cast (as operator).
|
|
919
|
+
*/
|
|
920
|
+
private async analyzeTypeCast(node: TypeCastNode, context: AnalysisContext): Promise<InternalAnalysisResult> {
|
|
921
|
+
const exprResult = await this.analyzeNode(node.expression, context);
|
|
922
|
+
const diagnostics = [...exprResult.diagnostics];
|
|
923
|
+
|
|
924
|
+
// ModelProvider requirement for non-primitive target types
|
|
925
|
+
const primitiveTypes = ['String', 'Integer', 'Decimal', 'Boolean', 'Date', 'DateTime', 'Time', 'Quantity'];
|
|
926
|
+
if (!context.modelProvider && !primitiveTypes.includes(node.targetType)) {
|
|
927
|
+
diagnostics.push(toDiagnostic(Errors.modelProviderRequired('as', node.range)));
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Check if casting from a union type
|
|
931
|
+
if (isUnionType(exprResult.type)) {
|
|
932
|
+
const targetTypeName = node.targetType;
|
|
933
|
+
const choices = getUnionChoices(exprResult.type);
|
|
934
|
+
if (choices.length > 0 && !choices.includes(targetTypeName)) {
|
|
935
|
+
diagnostics.push({
|
|
936
|
+
severity: DiagnosticSeverity.Warning,
|
|
937
|
+
code: 'invalid-type-cast',
|
|
938
|
+
message: `Type cast 'as ${targetTypeName}' may fail - type not present in union. Available types: ${choices.join(', ')}`,
|
|
939
|
+
range: node.range
|
|
940
|
+
});
|
|
869
941
|
}
|
|
870
942
|
}
|
|
871
943
|
|
|
872
|
-
//
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
const typeInfo = await this.modelProvider.getType(node.name);
|
|
878
|
-
if (typeInfo) {
|
|
879
|
-
return typeInfo;
|
|
880
|
-
}
|
|
881
|
-
}
|
|
944
|
+
// Type cast changes the type
|
|
945
|
+
const targetType: TypeInfo = {
|
|
946
|
+
type: node.targetType as TypeName,
|
|
947
|
+
singleton: exprResult.type.singleton
|
|
948
|
+
};
|
|
882
949
|
|
|
883
|
-
return {
|
|
950
|
+
return {
|
|
951
|
+
type: targetType,
|
|
952
|
+
diagnostics
|
|
953
|
+
};
|
|
884
954
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
switch (node.name) {
|
|
895
|
-
case '$this':
|
|
896
|
-
return { type: 'Any', singleton: false };
|
|
897
|
-
case '$index':
|
|
898
|
-
return { type: 'Integer', singleton: true };
|
|
899
|
-
case '$total':
|
|
900
|
-
return { type: 'Any', singleton: false };
|
|
901
|
-
default:
|
|
902
|
-
return { type: 'Any', singleton: false };
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
// Special FHIRPath environment variables
|
|
907
|
-
if (node.name === '%context' || node.name === '%resource' || node.name === '%rootResource') {
|
|
908
|
-
return { type: 'Any', singleton: false }; // These return the original input
|
|
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
|
-
}
|
|
916
|
-
|
|
917
|
-
const userType = this.userVariableTypes.get(varName);
|
|
918
|
-
if (userType) {
|
|
919
|
-
return userType;
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
return { type: 'Any', singleton: true };
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Analyzes error nodes.
|
|
958
|
+
*/
|
|
959
|
+
private analyzeError(node: ErrorNode, context: AnalysisContext): InternalAnalysisResult {
|
|
960
|
+
return {
|
|
961
|
+
type: { type: 'Any', singleton: false },
|
|
962
|
+
diagnostics: [this.createError(node, node.message, ErrorCodes.INVALID_SYNTAX)]
|
|
963
|
+
};
|
|
923
964
|
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
t.namespace === firstType.namespace &&
|
|
939
|
-
t.name === firstType.name
|
|
940
|
-
);
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Analyzes cursor nodes for completion.
|
|
968
|
+
*/
|
|
969
|
+
private analyzeCursorNode(node: AnyCursorNode, context: AnalysisContext): InternalAnalysisResult {
|
|
970
|
+
// Store cursor context for completion
|
|
971
|
+
if (this.cursorMode) {
|
|
972
|
+
this.stoppedAtCursor = true;
|
|
973
|
+
this.cursorContext = {
|
|
974
|
+
typeBeforeCursor: context.inputType,
|
|
975
|
+
cursorNode: node,
|
|
976
|
+
expectedType: undefined,
|
|
977
|
+
functionCall: undefined
|
|
978
|
+
};
|
|
941
979
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
if (fhirPathTypes.includes(targetType)) {
|
|
965
|
-
return { type: targetType as TypeName, singleton: true };
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
return { type: 'Any', singleton: true };
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
private isTypeCompatible(source: TypeInfo, target: TypeInfo): boolean {
|
|
972
|
-
// Exact match
|
|
973
|
-
if (source.type === target.type && source.singleton === target.singleton) {
|
|
974
|
-
return true;
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
// Any is compatible with everything
|
|
978
|
-
if (source.type === 'Any' || target.type === 'Any') {
|
|
979
|
-
return true;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
// Singleton can be promoted to collection
|
|
983
|
-
if (source.singleton && !target.singleton && source.type === target.type) {
|
|
984
|
-
return true;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
// Type hierarchy compatibility
|
|
988
|
-
if (this.isSubtypeOf(source.type, target.type)) {
|
|
989
|
-
// Check singleton compatibility
|
|
990
|
-
if (source.singleton === target.singleton || (source.singleton && !target.singleton)) {
|
|
991
|
-
return true;
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
// Numeric type compatibility
|
|
996
|
-
if (this.isNumericType(source.type) && this.isNumericType(target.type)) {
|
|
997
|
-
// Integer can be used where Decimal is expected
|
|
998
|
-
if (source.type === 'Integer' && target.type === 'Decimal') {
|
|
999
|
-
return source.singleton !== undefined && target.singleton !== undefined &&
|
|
1000
|
-
(source.singleton === target.singleton || (source.singleton && !target.singleton));
|
|
980
|
+
// Set expected type based on cursor context
|
|
981
|
+
if (node.context === CursorContext.Index) {
|
|
982
|
+
// Index expects an integer
|
|
983
|
+
this.cursorContext.expectedType = { type: 'Integer', singleton: true };
|
|
984
|
+
} else if (node.context === CursorContext.Argument) {
|
|
985
|
+
// Arguments context - check if we're in a function
|
|
986
|
+
const parent = (node as any).parent;
|
|
987
|
+
if (parent && parent.type === NodeType.Function) {
|
|
988
|
+
const funcNode = parent as FunctionNode;
|
|
989
|
+
if (funcNode.name.type === NodeType.Identifier) {
|
|
990
|
+
const funcName = (funcNode.name as IdentifierNode).name;
|
|
991
|
+
const funcDef = registry.getFunction(funcName);
|
|
992
|
+
if (funcDef) {
|
|
993
|
+
// Find argument index
|
|
994
|
+
const argIndex = funcNode.arguments.indexOf(node);
|
|
995
|
+
this.cursorContext.functionCall = {
|
|
996
|
+
definition: funcDef,
|
|
997
|
+
argumentIndex: argIndex >= 0 ? argIndex : 0
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1001
1002
|
}
|
|
1002
1003
|
}
|
|
1003
1004
|
|
|
1004
|
-
return
|
|
1005
|
+
return {
|
|
1006
|
+
type: { type: 'Any', singleton: false },
|
|
1007
|
+
diagnostics: []
|
|
1008
|
+
};
|
|
1005
1009
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1010
|
+
|
|
1011
|
+
// Legacy union combiner removed; union handled in analyzeBinary
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Helper to create diagnostic errors.
|
|
1015
|
+
*/
|
|
1016
|
+
private createError(node: ASTNode, message: string, code?: string): Diagnostic {
|
|
1017
|
+
return {
|
|
1018
|
+
range: node.range,
|
|
1019
|
+
message,
|
|
1020
|
+
severity: DiagnosticSeverity.Error,
|
|
1021
|
+
code,
|
|
1022
|
+
source: 'fhirpath'
|
|
1023
|
+
};
|
|
1018
1024
|
}
|
|
1019
1025
|
|
|
1020
|
-
private
|
|
1021
|
-
return
|
|
1026
|
+
private createWarning(node: ASTNode, message: string, code?: string): Diagnostic {
|
|
1027
|
+
return {
|
|
1028
|
+
range: node.range,
|
|
1029
|
+
message,
|
|
1030
|
+
severity: DiagnosticSeverity.Warning,
|
|
1031
|
+
code,
|
|
1032
|
+
source: 'fhirpath'
|
|
1033
|
+
};
|
|
1022
1034
|
}
|
|
1023
|
-
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Helper method to infer TypeInfo from runtime values (used for user variables).
|
|
1038
|
+
*/
|
|
1024
1039
|
private inferValueType(value: any): TypeInfo {
|
|
1025
1040
|
if (Array.isArray(value)) {
|
|
1026
1041
|
if (value.length === 0) {
|
|
@@ -1034,7 +1049,9 @@ export class Analyzer {
|
|
|
1034
1049
|
if (typeof value === 'string') {
|
|
1035
1050
|
return { type: 'String', singleton: true };
|
|
1036
1051
|
} else if (typeof value === 'number') {
|
|
1037
|
-
return
|
|
1052
|
+
return Number.isInteger(value)
|
|
1053
|
+
? { type: 'Integer', singleton: true }
|
|
1054
|
+
: { type: 'Decimal', singleton: true };
|
|
1038
1055
|
} else if (typeof value === 'boolean') {
|
|
1039
1056
|
return { type: 'Boolean', singleton: true };
|
|
1040
1057
|
} else if (value instanceof Date) {
|
|
@@ -1043,359 +1060,55 @@ export class Analyzer {
|
|
|
1043
1060
|
return { type: 'Any', singleton: true };
|
|
1044
1061
|
}
|
|
1045
1062
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1063
|
+
|
|
1064
|
+
async analyze(
|
|
1065
|
+
ast: ASTNode,
|
|
1066
|
+
userVariables?: Record<string, any>,
|
|
1049
1067
|
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
|
-
}
|
|
1068
|
+
options?: AnalyzerOptions
|
|
1069
|
+
): Promise<AnalysisResultWithCursor> {
|
|
1070
|
+
this.cursorMode = options?.cursorMode ?? false;
|
|
1071
|
+
this.stoppedAtCursor = false;
|
|
1072
|
+
this.cursorContext = undefined;
|
|
1083
1073
|
|
|
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 || [];
|
|
1074
|
+
// Create initial context with system and user variables
|
|
1075
|
+
const systemVars = new Map<string, TypeInfo>();
|
|
1076
|
+
// $this should be the input type (the root context), not Any
|
|
1077
|
+
systemVars.set('$this', inputType || { type: 'Any', singleton: false });
|
|
1078
|
+
systemVars.set('$index', { type: 'Integer', singleton: true });
|
|
1079
|
+
systemVars.set('$total', { type: 'Any', singleton: false });
|
|
1095
1080
|
|
|
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
|
-
);
|
|
1081
|
+
const userVars = new Map<string, TypeInfo>();
|
|
1082
|
+
if (userVariables) {
|
|
1083
|
+
Object.keys(userVariables).forEach(name => {
|
|
1084
|
+
const value = userVariables[name];
|
|
1085
|
+
if (value !== undefined && value !== null) {
|
|
1086
|
+
userVars.set(name, this.inferValueType(value));
|
|
1108
1087
|
}
|
|
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}`;
|
|
1088
|
+
});
|
|
1117
1089
|
}
|
|
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
1090
|
|
|
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
|
-
}
|
|
1091
|
+
// Create context with analyzeNode callback and model provider
|
|
1092
|
+
const initialContext = new AnalysisContext(
|
|
1093
|
+
inputType || { type: 'Any', singleton: false },
|
|
1094
|
+
systemVars,
|
|
1095
|
+
userVars,
|
|
1096
|
+
(node, ctx) => this.analyzeNode(node, ctx),
|
|
1097
|
+
this.modelProvider
|
|
1098
|
+
);
|
|
1171
1099
|
|
|
1172
|
-
//
|
|
1173
|
-
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1100
|
+
// Run context-flow analysis
|
|
1101
|
+
const result = await this.analyzeNode(ast, initialContext);
|
|
1176
1102
|
|
|
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
|
-
}
|
|
1103
|
+
// Legacy annotateAST/visitor path removed from default analysis to avoid duplication.
|
|
1192
1104
|
|
|
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
|
-
}
|
|
1105
|
+
return {
|
|
1106
|
+
diagnostics: result.diagnostics,
|
|
1107
|
+
ast,
|
|
1108
|
+
type: result.type,
|
|
1109
|
+
userVariables: new Map(result.context?.userVariables || initialContext.userVariables),
|
|
1110
|
+
stoppedAtCursor: this.cursorMode ? this.stoppedAtCursor : undefined,
|
|
1111
|
+
cursorContext: this.cursorMode ? this.cursorContext : undefined
|
|
1112
|
+
};
|
|
1400
1113
|
}
|
|
1401
|
-
}
|
|
1114
|
+
}
|