fhirpath-ts 0.1.0 → 0.2.0
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 +50 -18
- package/dist/analyzer/analyze-dto.d.ts +4 -0
- package/dist/analyzer/analyze-dto.d.ts.map +1 -1
- package/dist/analyzer/analyze-dto.js +12 -14
- package/dist/analyzer/analyze-dto.js.map +1 -1
- package/dist/analyzer/analyze.d.ts +22 -6
- package/dist/analyzer/analyze.d.ts.map +1 -1
- package/dist/analyzer/analyze.js +242 -98
- package/dist/analyzer/analyze.js.map +1 -1
- package/dist/analyzer/declarations.d.ts +26 -0
- package/dist/analyzer/declarations.d.ts.map +1 -0
- package/dist/analyzer/declarations.js +68 -0
- package/dist/analyzer/declarations.js.map +1 -0
- package/dist/analyzer/operator-rules.d.ts +127 -0
- package/dist/analyzer/operator-rules.d.ts.map +1 -0
- package/dist/analyzer/operator-rules.js +70 -0
- package/dist/analyzer/operator-rules.js.map +1 -0
- package/dist/analyzer/signatures.d.ts +1097 -10
- package/dist/analyzer/signatures.d.ts.map +1 -1
- package/dist/analyzer/signatures.js +70 -29
- package/dist/analyzer/signatures.js.map +1 -1
- package/dist/api/compile.d.ts +32 -8
- package/dist/api/compile.d.ts.map +1 -1
- package/dist/api/compile.js +2 -0
- package/dist/api/compile.js.map +1 -1
- package/dist/api/engine.d.ts +42 -16
- package/dist/api/engine.d.ts.map +1 -1
- package/dist/api/engine.js +2 -1
- package/dist/api/engine.js.map +1 -1
- package/dist/api/project.d.ts +8 -8
- package/dist/api/project.d.ts.map +1 -1
- package/dist/api/project.js +31 -19
- package/dist/api/project.js.map +1 -1
- package/dist/api/strict.d.ts +6 -0
- package/dist/api/strict.d.ts.map +1 -0
- package/dist/api/strict.js +71 -0
- package/dist/api/strict.js.map +1 -0
- package/dist/api/tagged.d.ts +4 -4
- package/dist/api/tagged.d.ts.map +1 -1
- package/dist/api/tagged.js.map +1 -1
- package/dist/cli/dto-check.js +7 -3
- package/dist/cli/dto-check.js.map +1 -1
- package/dist/engine/context.d.ts +3 -1
- package/dist/engine/context.d.ts.map +1 -1
- package/dist/engine/context.js +5 -1
- package/dist/engine/context.js.map +1 -1
- package/dist/engine/navigation.d.ts.map +1 -1
- package/dist/engine/navigation.js +7 -48
- package/dist/engine/navigation.js.map +1 -1
- package/dist/engine/type-matching.d.ts.map +1 -1
- package/dist/engine/type-matching.js +6 -16
- package/dist/engine/type-matching.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/parser/parser.d.ts.map +1 -1
- package/dist/parser/parser.js +102 -73
- package/dist/parser/parser.js.map +1 -1
- package/dist/parser/precedence.d.ts +368 -4
- package/dist/parser/precedence.d.ts.map +1 -1
- package/dist/parser/precedence.js +146 -32
- package/dist/parser/precedence.js.map +1 -1
- package/dist/r4/generated/type-maps.d.ts +1367 -0
- package/dist/r4/generated/type-maps.d.ts.map +1 -1
- package/dist/r4/index.d.ts +3 -1
- package/dist/r4/index.d.ts.map +1 -1
- package/dist/typed/context-maps.d.ts +23 -0
- package/dist/typed/context-maps.d.ts.map +1 -0
- package/dist/typed/context-maps.js +2 -0
- package/dist/typed/context-maps.js.map +1 -0
- package/dist/typed/infer.d.ts +99 -229
- package/dist/typed/infer.d.ts.map +1 -1
- package/dist/typed/infer.js +1 -84
- package/dist/typed/infer.js.map +1 -1
- package/dist/typed/inference-limits.d.ts +7 -0
- package/dist/typed/inference-limits.d.ts.map +1 -0
- package/dist/typed/inference-limits.js +6 -0
- package/dist/typed/inference-limits.js.map +1 -0
- package/dist/typed/metadata-compact.d.ts +189 -0
- package/dist/typed/metadata-compact.d.ts.map +1 -0
- package/dist/typed/metadata-compact.js +3 -0
- package/dist/typed/metadata-compact.js.map +1 -0
- package/dist/typed/parser.d.ts +515 -0
- package/dist/typed/parser.d.ts.map +1 -0
- package/dist/typed/parser.js +2 -0
- package/dist/typed/parser.js.map +1 -0
- package/dist/values/type-compat.d.ts +8 -0
- package/dist/values/type-compat.d.ts.map +1 -1
- package/dist/values/type-compat.js +44 -1
- package/dist/values/type-compat.js.map +1 -1
- package/package.json +20 -1
- package/src/analyzer/analyze-dto.ts +21 -17
- package/src/analyzer/analyze.ts +335 -109
- package/src/analyzer/declarations.ts +120 -0
- package/src/analyzer/operator-rules.ts +88 -0
- package/src/analyzer/signatures.ts +99 -41
- package/src/api/compile.ts +66 -11
- package/src/api/engine.ts +97 -33
- package/src/api/project.ts +54 -30
- package/src/api/strict.ts +112 -0
- package/src/api/tagged.ts +5 -5
- package/src/cli/dto-check.ts +7 -3
- package/src/engine/context.ts +7 -2
- package/src/engine/navigation.ts +7 -52
- package/src/engine/type-matching.ts +11 -17
- package/src/index.ts +25 -2
- package/src/parser/parser.ts +106 -74
- package/src/parser/precedence.ts +250 -33
- package/src/r4/generated/type-maps.ts +1557 -0
- package/src/typed/context-maps.ts +41 -0
- package/src/typed/infer.ts +157 -425
- package/src/typed/inference-limits.ts +9 -0
- package/src/typed/metadata-compact.ts +307 -0
- package/src/typed/parser.ts +2277 -0
- package/src/values/type-compat.ts +48 -1
- package/dist/typed/perf-fixture.types.d.ts +0 -136
- package/dist/typed/perf-fixture.types.d.ts.map +0 -1
- package/dist/typed/perf-fixture.types.js +0 -2
- package/dist/typed/perf-fixture.types.js.map +0 -1
- package/src/typed/perf-fixture.types.ts +0 -143
package/dist/analyzer/analyze.js
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import "../functions/install.js";
|
|
2
|
-
import { BUILTIN_ENV_VARIABLE_NAMES } from "../engine/context.js";
|
|
2
|
+
import { bareEnvironmentName, BUILTIN_ENV_VARIABLE_NAMES, normalizeEnvKeys } from "../engine/context.js";
|
|
3
3
|
import { FhirPathSyntaxError } from "../errors.js";
|
|
4
4
|
import { describeArity, functions } from "../functions/registry.js";
|
|
5
5
|
import { parse } from "../parser/parser.js";
|
|
6
|
-
import { unsatisfiedInput,
|
|
6
|
+
import { canonicalFocusType, commonValueKind, resolveByInput, resolveSystemTypeName, rootTypeMatches, SYSTEM_TYPE_LOCAL_NAMES, unsatisfiedInput, } from "../values/type-compat.js";
|
|
7
7
|
import { FHIR_PRIMITIVE_TO_SYSTEM, typeLocalName } from "../values/typed-value.js";
|
|
8
|
+
import { analyzerEnvironmentVariables, isRuntimeAnalyzerVariable, runtimeAnalyzerEnvironmentVariables, } from "./declarations.js";
|
|
9
|
+
import { applyOperatorResultRule, applyTypeOperatorResultRule } from "./operator-rules.js";
|
|
8
10
|
import { hasNestedUnboundedQuantifier } from "./regex-safety.js";
|
|
9
|
-
import { FUNCTION_SIGNATURES, singleAnd, unionStates, withSingle, } from "./signatures.js";
|
|
11
|
+
import { applyResultRule, FUNCTION_SIGNATURES, singleAnd, unionStates, withSingle, } from "./signatures.js";
|
|
10
12
|
const UNKNOWN = { types: undefined, single: undefined };
|
|
13
|
+
/** A runtime-analyzed function result no longer has exact facts about its input focus. */
|
|
14
|
+
function callResult(state, runtime) {
|
|
15
|
+
return runtime
|
|
16
|
+
? {
|
|
17
|
+
types: state.types,
|
|
18
|
+
single: state.single,
|
|
19
|
+
...(state.targets !== undefined && { targets: state.targets }),
|
|
20
|
+
}
|
|
21
|
+
: state;
|
|
22
|
+
}
|
|
11
23
|
function emptyScope() {
|
|
12
24
|
return { vars: new Map(), hasDynamic: false };
|
|
13
25
|
}
|
|
@@ -77,7 +89,11 @@ export function analyzeExpressionDetailed(expression, options) {
|
|
|
77
89
|
result: { types: undefined, single: undefined },
|
|
78
90
|
};
|
|
79
91
|
}
|
|
80
|
-
|
|
92
|
+
return analyzeAstDetailed(ast, options);
|
|
93
|
+
}
|
|
94
|
+
/** Analyze an already parsed expression, optionally with a runtime-derived input state. */
|
|
95
|
+
function analyzeAstDetailed(ast, options, root) {
|
|
96
|
+
const analyzer = new Analyzer(options, root);
|
|
81
97
|
const state = analyzer.walk(ast, analyzer.rootState(), emptyScope());
|
|
82
98
|
return {
|
|
83
99
|
diagnostics: analyzer.diagnostics,
|
|
@@ -85,30 +101,43 @@ export function analyzeExpressionDetailed(expression, options) {
|
|
|
85
101
|
result: { types: state.types, single: state.single },
|
|
86
102
|
};
|
|
87
103
|
}
|
|
104
|
+
/** Analyze a parsed expression using facts from the actual evaluation focus. */
|
|
105
|
+
export function analyzeRuntimeAstDetailed(ast, options, root) {
|
|
106
|
+
return analyzeAstDetailed(ast, options, root);
|
|
107
|
+
}
|
|
88
108
|
class Analyzer {
|
|
89
109
|
diagnostics = [];
|
|
90
110
|
dependencies = new Set();
|
|
91
111
|
model;
|
|
92
|
-
|
|
112
|
+
root;
|
|
113
|
+
runtime;
|
|
93
114
|
frames = [];
|
|
94
115
|
/** Every declaration of each host-supplied name; one entry unless the name is overloaded. */
|
|
95
116
|
customFunctions;
|
|
96
117
|
declaredVariables;
|
|
97
|
-
|
|
118
|
+
activeExpressionFunctions = new Set();
|
|
119
|
+
constructor(options, root) {
|
|
98
120
|
this.model = options?.model;
|
|
121
|
+
this.runtime = root !== undefined;
|
|
99
122
|
const inputType = options?.inputType;
|
|
100
|
-
this.
|
|
123
|
+
this.root =
|
|
124
|
+
root ??
|
|
125
|
+
(inputType === undefined
|
|
126
|
+
? { types: undefined, single: undefined }
|
|
127
|
+
: { types: [this.model?.resolveType(inputType) ?? inputType], single: true });
|
|
101
128
|
this.customFunctions = new Map(Object.entries(options?.functions ?? {}).map(([name, declared]) => [
|
|
102
129
|
name,
|
|
103
|
-
('overloads' in declared ? declared.overloads : [declared]).map(resolvedDeclaration),
|
|
104
|
-
]));
|
|
105
|
-
this.declaredVariables = new Map(Object.entries(options?.variables ?? {}).map(([name, variable]) => [
|
|
106
|
-
name.startsWith('%') ? name.slice(1) : name,
|
|
107
|
-
variable,
|
|
130
|
+
('overloads' in declared ? declared.overloads : [declared]).map(declaration => resolvedDeclaration(declaration, this.model, this.runtime)),
|
|
108
131
|
]));
|
|
132
|
+
this.declaredVariables = new Map(Object.entries(normalizeEnvKeys(options?.variables)));
|
|
109
133
|
}
|
|
110
134
|
rootState() {
|
|
111
|
-
return
|
|
135
|
+
return {
|
|
136
|
+
types: this.root.types,
|
|
137
|
+
single: this.root.single,
|
|
138
|
+
rawInput: true,
|
|
139
|
+
...(this.root.exactTypes !== undefined && { exactTypes: this.root.exactTypes }),
|
|
140
|
+
};
|
|
112
141
|
}
|
|
113
142
|
walk(node, input, scope) {
|
|
114
143
|
switch (node.kind) {
|
|
@@ -181,7 +210,7 @@ class Analyzer {
|
|
|
181
210
|
}
|
|
182
211
|
const declared = this.declaredVariables.get(node.name);
|
|
183
212
|
if (declared !== undefined) {
|
|
184
|
-
return
|
|
213
|
+
return this.declaredVariableState(declared);
|
|
185
214
|
}
|
|
186
215
|
switch (node.name) {
|
|
187
216
|
case 'context':
|
|
@@ -228,26 +257,50 @@ class Analyzer {
|
|
|
228
257
|
if (input.types !== undefined && input.types.length === 0) {
|
|
229
258
|
return { types: [], single: true };
|
|
230
259
|
}
|
|
260
|
+
const exactTypes = input.exactTypes?.filter(type => rootTypeMatches(this.model, type, node.name));
|
|
261
|
+
if (exactTypes !== undefined && exactTypes.length > 0) {
|
|
262
|
+
const types = input.types?.filter(type => rootTypeMatches(this.model, type, node.name));
|
|
263
|
+
return {
|
|
264
|
+
types: types === undefined ? undefined : [...new Set(types)],
|
|
265
|
+
single: input.single,
|
|
266
|
+
exactTypes,
|
|
267
|
+
...(input.rawInput === true && { rawInput: true }),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
231
270
|
if (input.types === undefined) {
|
|
232
271
|
// Even with an unknown input, a root identifier naming a model type anchors
|
|
233
272
|
// the state — this is what checks `Patient.nope` without an inputType option.
|
|
234
|
-
const asType = this.model?.resolveType(node.name);
|
|
273
|
+
const asType = this.model?.resolveType(node.name) ?? resolveSystemTypeName(node.name);
|
|
235
274
|
if (asType !== undefined) {
|
|
236
|
-
return {
|
|
275
|
+
return {
|
|
276
|
+
types: [asType],
|
|
277
|
+
single: true,
|
|
278
|
+
...(input.exactTypes !== undefined && {
|
|
279
|
+
exactTypes: input.exactTypes.filter(type => this.model?.isSubtypeOf(type, asType) === true),
|
|
280
|
+
}),
|
|
281
|
+
};
|
|
237
282
|
}
|
|
238
283
|
return UNKNOWN;
|
|
239
284
|
}
|
|
240
285
|
// Root rule: an identifier naming the (super)type of the context is the context.
|
|
241
|
-
|
|
242
|
-
const asType = this.model
|
|
243
|
-
|
|
286
|
+
{
|
|
287
|
+
const asType = this.model?.resolveType(node.name) ?? resolveSystemTypeName(node.name);
|
|
288
|
+
let matchingTypes = [];
|
|
289
|
+
if (asType !== undefined) {
|
|
290
|
+
matchingTypes = input.types.filter(type => rootTypeMatches(this.model, type, node.name));
|
|
291
|
+
}
|
|
292
|
+
if (asType !== undefined && matchingTypes.length > 0) {
|
|
244
293
|
// The runtime matches a type name against the raw input only through the
|
|
245
294
|
// resourceType discriminator (values/typed-value.ts), so a non-resource
|
|
246
295
|
// name never matches there and the whole path navigates to empty.
|
|
247
|
-
if (input.rawInput === true && !this.isResourceType(asType)) {
|
|
296
|
+
if (input.rawInput === true && !asType.startsWith('System.') && !this.isResourceType(asType)) {
|
|
248
297
|
this.report('datatype-root', `'${node.name}' is not a resource type, and a type-name root matches only a resource's resourceType, so this always evaluates to empty — navigate from the input with a relative path`, node.span);
|
|
249
298
|
}
|
|
250
|
-
return
|
|
299
|
+
return {
|
|
300
|
+
...input,
|
|
301
|
+
types: matchingTypes,
|
|
302
|
+
...(input.exactTypes !== undefined && { exactTypes: input.exactTypes }),
|
|
303
|
+
};
|
|
251
304
|
}
|
|
252
305
|
}
|
|
253
306
|
const found = [];
|
|
@@ -371,7 +424,8 @@ class Analyzer {
|
|
|
371
424
|
const signature = registered !== undefined ? FUNCTION_SIGNATURES[node.name] : this.toSignature(custom?.signature);
|
|
372
425
|
if (!signature) {
|
|
373
426
|
this.walkUncheckedArguments(node, input, scope);
|
|
374
|
-
|
|
427
|
+
const expressionResult = this.expressionFunctionResult(node.name, custom, input, scope);
|
|
428
|
+
return expressionResult === undefined ? UNKNOWN : callResult(expressionResult, this.runtime);
|
|
375
429
|
}
|
|
376
430
|
this.checkCallInput(node, signature, input);
|
|
377
431
|
const { argStates, typeTarget } = this.walkArguments(node, signature, input, scope);
|
|
@@ -384,7 +438,105 @@ class Analyzer {
|
|
|
384
438
|
if ((node.name === 'ofType' || node.name === 'as') && typeTarget !== undefined) {
|
|
385
439
|
return { types: this.narrowTypes(input, typeTarget, node.span), single: input.single };
|
|
386
440
|
}
|
|
387
|
-
|
|
441
|
+
const expressionResult = this.expressionFunctionResult(node.name, custom, input, scope);
|
|
442
|
+
if (expressionResult !== undefined) {
|
|
443
|
+
return callResult(expressionResult, this.runtime);
|
|
444
|
+
}
|
|
445
|
+
return callResult(applyResultRule(signature.result, input, argStates), this.runtime);
|
|
446
|
+
}
|
|
447
|
+
/** Apply the expression-body and criteria rules in one place for signed and unsigned declarations. */
|
|
448
|
+
expressionFunctionResult(name, declaration, input, scope) {
|
|
449
|
+
if (declaration?.expression === undefined) {
|
|
450
|
+
for (const possible of declaration?.possibleBodies ?? []) {
|
|
451
|
+
if (possible.expression !== undefined) {
|
|
452
|
+
this.walkExpressionFunction(name, possible.expression, possible.variables, this.expressionBodyInput(possible, input), scope);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
const expression = declaration.expression;
|
|
458
|
+
const bodyInput = this.expressionBodyInput(declaration, input);
|
|
459
|
+
if (declaration.criteria === true) {
|
|
460
|
+
this.walkExpressionFunction(name, expression, declaration.variables, bodyInput, scope);
|
|
461
|
+
return { types: ['System.Boolean'], single: true };
|
|
462
|
+
}
|
|
463
|
+
if (declaration.signature?.result === undefined) {
|
|
464
|
+
return this.walkExpressionFunction(name, expression, declaration.variables, bodyInput, scope);
|
|
465
|
+
}
|
|
466
|
+
if (this.runtime) {
|
|
467
|
+
this.walkExpressionFunction(name, expression, declaration.variables, bodyInput, scope);
|
|
468
|
+
}
|
|
469
|
+
return undefined;
|
|
470
|
+
}
|
|
471
|
+
/** A declared input type is the contract an expression body must satisfy. */
|
|
472
|
+
expressionBodyInput(declaration, callInput) {
|
|
473
|
+
const declared = declaration.signature?.input?.types;
|
|
474
|
+
if (declared === undefined) {
|
|
475
|
+
return callInput;
|
|
476
|
+
}
|
|
477
|
+
const types = declared
|
|
478
|
+
.map(type => this.model === undefined ? resolveSystemTypeName(typeLocalName(type)) : canonicalFocusType(this.model, type))
|
|
479
|
+
.filter((type) => type !== undefined);
|
|
480
|
+
if (types.length === 0) {
|
|
481
|
+
return callInput;
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
types: [...new Set(types)],
|
|
485
|
+
single: callInput.single,
|
|
486
|
+
...(callInput.targets !== undefined && { targets: callInput.targets }),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/** Infer a literal expression-function body under its call focus and temporary local environment declarations. */
|
|
490
|
+
walkExpressionFunction(name, expression, variables, input, callerScope) {
|
|
491
|
+
if (this.activeExpressionFunctions.has(name)) {
|
|
492
|
+
return UNKNOWN;
|
|
493
|
+
}
|
|
494
|
+
let ast = expression.ast;
|
|
495
|
+
if (ast === undefined) {
|
|
496
|
+
try {
|
|
497
|
+
ast = parse(expression.source);
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
return UNKNOWN;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const functionScope = forkScope(callerScope);
|
|
504
|
+
for (const [declaredName, variable] of Object.entries(variables ?? {})) {
|
|
505
|
+
const bare = bareEnvironmentName(declaredName);
|
|
506
|
+
// defineVariable() values have priority over environment overlays at runtime.
|
|
507
|
+
if (!functionScope.vars.has(bare)) {
|
|
508
|
+
functionScope.vars.set(bare, this.declaredVariableState(variable));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const diagnosticCount = this.diagnostics.length;
|
|
512
|
+
this.activeExpressionFunctions.add(name);
|
|
513
|
+
this.frames.push(input);
|
|
514
|
+
try {
|
|
515
|
+
return this.walk(ast, input, functionScope);
|
|
516
|
+
}
|
|
517
|
+
finally {
|
|
518
|
+
if (this.runtime) {
|
|
519
|
+
for (const diagnostic of this.diagnostics.slice(diagnosticCount)) {
|
|
520
|
+
diagnostic.message = `Custom function '${name}': ${diagnostic.message}`;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
// Body spans index another source string, so callers cannot display these diagnostics correctly.
|
|
525
|
+
this.diagnostics.length = diagnosticCount;
|
|
526
|
+
}
|
|
527
|
+
this.frames.pop();
|
|
528
|
+
this.activeExpressionFunctions.delete(name);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
/** Convert one declared environment value into the same state used for scoped variables. */
|
|
532
|
+
declaredVariableState(declared) {
|
|
533
|
+
const exactTypes = this.runtime && isRuntimeAnalyzerVariable(declared) ? declared.exactTypes : undefined;
|
|
534
|
+
return {
|
|
535
|
+
types: declared.types?.map(type => this.canonicalize(type)),
|
|
536
|
+
single: declared.single,
|
|
537
|
+
...(declared.targets !== undefined && { targets: declared.targets.map(type => this.canonicalize(type)) }),
|
|
538
|
+
...(exactTypes !== undefined && { exactTypes }),
|
|
539
|
+
};
|
|
388
540
|
}
|
|
389
541
|
/**
|
|
390
542
|
* Selects a host declaration from the focus. One match keeps its full
|
|
@@ -396,9 +548,18 @@ class Analyzer {
|
|
|
396
548
|
if (candidates === undefined || candidates.length <= 1) {
|
|
397
549
|
return candidates?.[0];
|
|
398
550
|
}
|
|
551
|
+
if (this.runtime && (input.exactTypes !== undefined || this.model === undefined)) {
|
|
552
|
+
const resolution = resolveByInput(this.model, candidates, candidate => candidate.signature?.input?.types, input.exactTypes ?? []);
|
|
553
|
+
return 'resolved' in resolution ? resolution.resolved : mergedDeclaration(candidates);
|
|
554
|
+
}
|
|
399
555
|
const focus = input.types ?? [];
|
|
400
556
|
const fitting = candidates.filter(candidate => unsatisfiedInput(this.model, candidate.signature?.input?.types, focus) === undefined);
|
|
401
|
-
|
|
557
|
+
if (fitting.length === 1) {
|
|
558
|
+
return fitting[0];
|
|
559
|
+
}
|
|
560
|
+
const possible = fitting.length === 0 ? candidates : fitting;
|
|
561
|
+
const merged = mergedDeclaration(possible);
|
|
562
|
+
return this.runtime ? { ...merged, possibleBodies: possible } : merged;
|
|
402
563
|
}
|
|
403
564
|
/** Without a signature the arguments still walk (for their own diagnostics), each in a scope fork. */
|
|
404
565
|
walkUncheckedArguments(node, input, scope) {
|
|
@@ -535,7 +696,11 @@ class Analyzer {
|
|
|
535
696
|
return {
|
|
536
697
|
...(declared.input !== undefined && { input: declared.input }),
|
|
537
698
|
...(declared.args !== undefined && { args: declared.args }),
|
|
538
|
-
result:
|
|
699
|
+
result: {
|
|
700
|
+
kind: 'fixed',
|
|
701
|
+
...(types !== undefined && { types }),
|
|
702
|
+
...(single !== undefined && { single }),
|
|
703
|
+
},
|
|
539
704
|
};
|
|
540
705
|
}
|
|
541
706
|
/**
|
|
@@ -580,7 +745,7 @@ class Analyzer {
|
|
|
580
745
|
/** Check a type name; returns its canonical form, or undefined when it was reported unknown. */
|
|
581
746
|
checkTypeName(parts, span) {
|
|
582
747
|
if (parts.length === 2 && parts[0] === 'System') {
|
|
583
|
-
if (!
|
|
748
|
+
if (!SYSTEM_TYPE_LOCAL_NAMES.has(parts[1])) {
|
|
584
749
|
this.report('unknown-type', `Unknown type 'System.${parts[1]}'`, span);
|
|
585
750
|
return undefined;
|
|
586
751
|
}
|
|
@@ -592,11 +757,11 @@ class Analyzer {
|
|
|
592
757
|
return undefined;
|
|
593
758
|
}
|
|
594
759
|
const resolved = this.model?.resolveType(name);
|
|
595
|
-
if (this.model && resolved === undefined && !
|
|
760
|
+
if (this.model && resolved === undefined && !SYSTEM_TYPE_LOCAL_NAMES.has(name)) {
|
|
596
761
|
this.report('unknown-type', `Unknown type '${parts.join('.')}'`, span);
|
|
597
762
|
return undefined;
|
|
598
763
|
}
|
|
599
|
-
return resolved ?? (
|
|
764
|
+
return resolved ?? (SYSTEM_TYPE_LOCAL_NAMES.has(name) ? `System.${name}` : name);
|
|
600
765
|
}
|
|
601
766
|
walkBinary(node, input, scope) {
|
|
602
767
|
// Operator operands are separate chains: each analyzes against its own scope
|
|
@@ -612,23 +777,14 @@ class Analyzer {
|
|
|
612
777
|
case 'div':
|
|
613
778
|
case 'mod': {
|
|
614
779
|
this.checkArithmetic(node.operator, left, right, node.span);
|
|
615
|
-
|
|
616
|
-
// plain division yields Decimal; everything else keeps the operand type.
|
|
617
|
-
const quantity = (node.operator === '*' || node.operator === '/') &&
|
|
618
|
-
(kindOf(left) === 'Quantity' || kindOf(right) === 'Quantity');
|
|
619
|
-
const types = quantity
|
|
620
|
-
? ['System.Quantity']
|
|
621
|
-
: node.operator === '/'
|
|
622
|
-
? ['System.Decimal']
|
|
623
|
-
: (left.types ?? right.types);
|
|
624
|
-
return { types, single: true };
|
|
780
|
+
return applyOperatorResultRule(node.operator, left, right);
|
|
625
781
|
}
|
|
626
782
|
case '&':
|
|
627
783
|
this.requireSingle(left, node.left.span, "'&' expects single-item operands");
|
|
628
784
|
this.requireSingle(right, node.right.span, "'&' expects single-item operands");
|
|
629
785
|
this.requireKind(left, 'String', node.left.span, "'&' expects String operands");
|
|
630
786
|
this.requireKind(right, 'String', node.right.span, "'&' expects String operands");
|
|
631
|
-
return
|
|
787
|
+
return applyOperatorResultRule(node.operator, left, right);
|
|
632
788
|
case '<':
|
|
633
789
|
case '>':
|
|
634
790
|
case '<=':
|
|
@@ -636,13 +792,13 @@ class Analyzer {
|
|
|
636
792
|
this.requireSingle(left, node.left.span, `Operator '${node.operator}' expects single-item operands`);
|
|
637
793
|
this.requireSingle(right, node.right.span, `Operator '${node.operator}' expects single-item operands`);
|
|
638
794
|
this.checkComparable(left, right, node.span);
|
|
639
|
-
return
|
|
795
|
+
return applyOperatorResultRule(node.operator, left, right);
|
|
640
796
|
case '=':
|
|
641
797
|
case '!=':
|
|
642
798
|
case '~':
|
|
643
799
|
case '!~':
|
|
644
800
|
this.checkEquality(left, right, node.span);
|
|
645
|
-
return
|
|
801
|
+
return applyOperatorResultRule(node.operator, left, right);
|
|
646
802
|
case 'and':
|
|
647
803
|
case 'or':
|
|
648
804
|
case 'xor':
|
|
@@ -651,24 +807,17 @@ class Analyzer {
|
|
|
651
807
|
// rule, so only cardinality is checkable here.
|
|
652
808
|
this.requireSingle(left, node.left.span, `'${node.operator}' expects single-item operands`);
|
|
653
809
|
this.requireSingle(right, node.right.span, `'${node.operator}' expects single-item operands`);
|
|
654
|
-
return
|
|
810
|
+
return applyOperatorResultRule(node.operator, left, right);
|
|
655
811
|
}
|
|
656
812
|
case '|': {
|
|
657
|
-
|
|
658
|
-
if (left.types?.length === 0) {
|
|
659
|
-
return right;
|
|
660
|
-
}
|
|
661
|
-
if (right.types?.length === 0) {
|
|
662
|
-
return left;
|
|
663
|
-
}
|
|
664
|
-
return withSingle(unionStates([left, right]), false);
|
|
813
|
+
return applyOperatorResultRule(node.operator, left, right);
|
|
665
814
|
}
|
|
666
815
|
case 'in':
|
|
667
816
|
case 'contains': {
|
|
668
817
|
const singletonSide = node.operator === 'in' ? left : right;
|
|
669
818
|
const singletonSpan = node.operator === 'in' ? node.left.span : node.right.span;
|
|
670
819
|
this.requireSingle(singletonSide, singletonSpan, `The ${node.operator === 'in' ? 'left' : 'right'} operand of '${node.operator}' must be a single item`);
|
|
671
|
-
return
|
|
820
|
+
return applyOperatorResultRule(node.operator, left, right);
|
|
672
821
|
}
|
|
673
822
|
/* v8 ignore start -- the parser produces no other binary operators */
|
|
674
823
|
default:
|
|
@@ -686,21 +835,16 @@ class Analyzer {
|
|
|
686
835
|
const operand = this.walk(node.operand, input, scope);
|
|
687
836
|
this.requireSingle(operand, node.operand.span, `'${node.operator}' expects a single item operand`);
|
|
688
837
|
const resolved = this.checkTypeName(node.type.parts, node.type.span);
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
}
|
|
692
|
-
if (resolved === undefined) {
|
|
693
|
-
return UNKNOWN;
|
|
694
|
-
}
|
|
695
|
-
return { types: this.narrowTypes(operand, resolved, node.span), single: true };
|
|
838
|
+
const narrowed = node.operator === 'as' && resolved !== undefined ? this.narrowTypes(operand, resolved, node.span) : undefined;
|
|
839
|
+
return applyTypeOperatorResultRule(node.operator, narrowed);
|
|
696
840
|
}
|
|
697
841
|
checkArithmetic(operator, left, right, span) {
|
|
698
842
|
if (isCollection(left) || isCollection(right)) {
|
|
699
843
|
this.report('singleton-required', `Operator '${operator}' expects single-item operands${NARROW_HINT}`, span);
|
|
700
844
|
return;
|
|
701
845
|
}
|
|
702
|
-
const leftKind =
|
|
703
|
-
const rightKind =
|
|
846
|
+
const leftKind = commonValueKind(left.types);
|
|
847
|
+
const rightKind = commonValueKind(right.types);
|
|
704
848
|
if (leftKind === undefined || rightKind === undefined || leftKind === 'Complex' || rightKind === 'Complex') {
|
|
705
849
|
return;
|
|
706
850
|
}
|
|
@@ -717,8 +861,8 @@ class Analyzer {
|
|
|
717
861
|
}
|
|
718
862
|
}
|
|
719
863
|
checkComparable(left, right, span) {
|
|
720
|
-
const leftKind =
|
|
721
|
-
const rightKind =
|
|
864
|
+
const leftKind = commonValueKind(left.types);
|
|
865
|
+
const rightKind = commonValueKind(right.types);
|
|
722
866
|
if (leftKind === undefined || rightKind === undefined) {
|
|
723
867
|
return;
|
|
724
868
|
}
|
|
@@ -727,8 +871,8 @@ class Analyzer {
|
|
|
727
871
|
}
|
|
728
872
|
}
|
|
729
873
|
checkEquality(left, right, span) {
|
|
730
|
-
const leftKind =
|
|
731
|
-
const rightKind =
|
|
874
|
+
const leftKind = commonValueKind(left.types);
|
|
875
|
+
const rightKind = commonValueKind(right.types);
|
|
732
876
|
if (leftKind === undefined || rightKind === undefined || leftKind === 'Complex' || rightKind === 'Complex') {
|
|
733
877
|
return;
|
|
734
878
|
}
|
|
@@ -737,7 +881,7 @@ class Analyzer {
|
|
|
737
881
|
}
|
|
738
882
|
}
|
|
739
883
|
requireKind(state, kind, span, message) {
|
|
740
|
-
const actual =
|
|
884
|
+
const actual = commonValueKind(state.types);
|
|
741
885
|
if (actual === undefined) {
|
|
742
886
|
return;
|
|
743
887
|
}
|
|
@@ -752,31 +896,48 @@ class Analyzer {
|
|
|
752
896
|
}
|
|
753
897
|
/** Functions whose first argument is a regular expression pattern. */
|
|
754
898
|
const REGEX_PATTERN_FUNCTIONS = new Set(['matches', 'matchesFull', 'replaceMatches']);
|
|
755
|
-
const SYSTEM_TYPE_NAMES = new Set([
|
|
756
|
-
'Any',
|
|
757
|
-
'Boolean',
|
|
758
|
-
'String',
|
|
759
|
-
'Integer',
|
|
760
|
-
'Long',
|
|
761
|
-
'Decimal',
|
|
762
|
-
'Date',
|
|
763
|
-
'DateTime',
|
|
764
|
-
'Time',
|
|
765
|
-
'Quantity',
|
|
766
|
-
]);
|
|
767
899
|
/** Appended to singleton-misuse messages so the fix is spelled out, not just the rule. */
|
|
768
900
|
const NARROW_HINT = ' — narrow it to one item with first(), last(), or single()';
|
|
769
901
|
/** Statically known to possibly hold more than one item (types known, not a singleton). */
|
|
770
902
|
function isCollection(state) {
|
|
771
903
|
return state.types !== undefined && state.single === false;
|
|
772
904
|
}
|
|
773
|
-
function resolvedDeclaration(declared) {
|
|
905
|
+
function resolvedDeclaration(declared, model, runtime) {
|
|
774
906
|
if (declared.expression === undefined) {
|
|
775
|
-
return
|
|
907
|
+
return {
|
|
908
|
+
...(declared.minArity !== undefined && { minArity: declared.minArity }),
|
|
909
|
+
...(declared.maxArity !== undefined && { maxArity: declared.maxArity }),
|
|
910
|
+
...(declared.signature !== undefined && { signature: declared.signature }),
|
|
911
|
+
};
|
|
776
912
|
}
|
|
777
913
|
// An expression-defined function takes no arguments, which is how the runtime
|
|
778
914
|
// calls it (`evaluateHostFunction`).
|
|
779
|
-
|
|
915
|
+
const expression = resolvedExpression(declared.expression);
|
|
916
|
+
return {
|
|
917
|
+
minArity: 0,
|
|
918
|
+
maxArity: 0,
|
|
919
|
+
...(declared.signature !== undefined && { signature: declared.signature }),
|
|
920
|
+
...(expression !== undefined && { expression }),
|
|
921
|
+
...(declared.criteria !== undefined && { criteria: declared.criteria }),
|
|
922
|
+
...((declared.env !== undefined || declared.envTypes !== undefined) && {
|
|
923
|
+
variables: runtime
|
|
924
|
+
? runtimeAnalyzerEnvironmentVariables(declared.env, declared.envTypes, model)
|
|
925
|
+
: analyzerEnvironmentVariables(declared.env, declared.envTypes, model),
|
|
926
|
+
}),
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function resolvedExpression(expression) {
|
|
930
|
+
if (typeof expression === 'string') {
|
|
931
|
+
return { source: expression };
|
|
932
|
+
}
|
|
933
|
+
const compiled = expression;
|
|
934
|
+
if (typeof compiled?.source !== 'string') {
|
|
935
|
+
return undefined;
|
|
936
|
+
}
|
|
937
|
+
return {
|
|
938
|
+
source: compiled.source,
|
|
939
|
+
...(compiled.ast !== undefined && { ast: compiled.ast }),
|
|
940
|
+
};
|
|
780
941
|
}
|
|
781
942
|
/**
|
|
782
943
|
* What several declarations of one name say together, for a call none of them
|
|
@@ -809,7 +970,7 @@ function mergedInput(candidates) {
|
|
|
809
970
|
/** The union of the declarations' results, unknown as soon as one of them is. */
|
|
810
971
|
function mergedResult(candidates) {
|
|
811
972
|
const union = unionStates(candidates.map(candidate => ({
|
|
812
|
-
types: candidate.signature?.result?.types,
|
|
973
|
+
types: candidate.signature?.result?.types === undefined ? undefined : [...candidate.signature.result.types],
|
|
813
974
|
single: candidate.signature?.result?.single,
|
|
814
975
|
})));
|
|
815
976
|
if (union.types === undefined && union.single === undefined) {
|
|
@@ -820,23 +981,6 @@ function mergedResult(candidates) {
|
|
|
820
981
|
...(union.single !== undefined && { single: union.single }),
|
|
821
982
|
};
|
|
822
983
|
}
|
|
823
|
-
/** The behavior kind shared by every candidate type, or undefined when mixed/unknown. */
|
|
824
|
-
function kindOf(state) {
|
|
825
|
-
if (state.types === undefined || state.types.length === 0) {
|
|
826
|
-
return undefined;
|
|
827
|
-
}
|
|
828
|
-
let kind;
|
|
829
|
-
for (const type of state.types) {
|
|
830
|
-
const typeKind = valueKindOfTypeName(type);
|
|
831
|
-
if (kind === undefined) {
|
|
832
|
-
kind = typeKind;
|
|
833
|
-
}
|
|
834
|
-
else if (kind !== typeKind) {
|
|
835
|
-
return undefined;
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
return kind;
|
|
839
|
-
}
|
|
840
984
|
function typeSpecifierParts(node) {
|
|
841
985
|
if (node.kind === 'identifier') {
|
|
842
986
|
return [node.name];
|