fhirpath-ts 0.2.1 → 0.2.2
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 +12 -5
- package/dist/analyzer/analyze-dto.d.ts.map +1 -1
- package/dist/analyzer/analyze-dto.js +14 -8
- package/dist/analyzer/analyze-dto.js.map +1 -1
- package/dist/analyzer/analyze.d.ts +23 -12
- package/dist/analyzer/analyze.d.ts.map +1 -1
- package/dist/analyzer/analyze.js +138 -46
- package/dist/analyzer/analyze.js.map +1 -1
- package/dist/analyzer/declarations.d.ts +6 -0
- package/dist/analyzer/declarations.d.ts.map +1 -1
- package/dist/analyzer/declarations.js +7 -0
- package/dist/analyzer/declarations.js.map +1 -1
- package/dist/analyzer/expression-policy.d.ts +88 -14
- package/dist/analyzer/expression-policy.d.ts.map +1 -1
- package/dist/analyzer/expression-policy.js +296 -41
- package/dist/analyzer/expression-policy.js.map +1 -1
- package/dist/analyzer/operator-rules.d.ts.map +1 -1
- package/dist/analyzer/operator-rules.js +7 -6
- package/dist/analyzer/operator-rules.js.map +1 -1
- package/dist/analyzer/signatures.d.ts +49 -0
- package/dist/analyzer/signatures.d.ts.map +1 -1
- package/dist/analyzer/signatures.js +76 -31
- package/dist/analyzer/signatures.js.map +1 -1
- package/dist/analyzer/source-options.d.ts +17 -0
- package/dist/analyzer/source-options.d.ts.map +1 -0
- package/dist/analyzer/source-options.js +3 -0
- package/dist/analyzer/source-options.js.map +1 -0
- package/dist/api/dto.d.ts +9 -4
- package/dist/api/dto.d.ts.map +1 -1
- package/dist/api/dto.js +14 -3
- package/dist/api/dto.js.map +1 -1
- package/dist/api/strict.d.ts.map +1 -1
- package/dist/api/strict.js +8 -3
- package/dist/api/strict.js.map +1 -1
- package/dist/cli/dto-check.d.ts +9 -0
- package/dist/cli/dto-check.d.ts.map +1 -1
- package/dist/cli/dto-check.js +49 -6
- package/dist/cli/dto-check.js.map +1 -1
- package/dist/cli/fhirpath-check.js +161 -31
- package/dist/cli/fhirpath-check.js.map +1 -1
- package/dist/eslint/index.d.ts.map +1 -1
- package/dist/eslint/index.js +13 -8
- package/dist/eslint/index.js.map +1 -1
- package/dist/sites/index.d.ts +31 -2
- package/dist/sites/index.d.ts.map +1 -1
- package/dist/sites/index.js +351 -30
- package/dist/sites/index.js.map +1 -1
- package/dist/typed/infer.d.ts +2 -0
- package/dist/typed/infer.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/analyzer/analyze-dto.ts +19 -9
- package/src/analyzer/analyze.ts +186 -54
- package/src/analyzer/declarations.ts +12 -0
- package/src/analyzer/expression-policy.ts +423 -47
- package/src/analyzer/operator-rules.ts +7 -6
- package/src/analyzer/signatures.ts +100 -36
- package/src/analyzer/source-options.ts +19 -0
- package/src/api/dto.ts +35 -9
- package/src/api/strict.ts +8 -3
- package/src/cli/dto-check.ts +56 -6
- package/src/cli/fhirpath-check.ts +180 -34
- package/src/eslint/index.ts +15 -12
- package/src/sites/index.ts +428 -40
- package/src/typed/infer.ts +2 -2
|
@@ -4,6 +4,8 @@ export interface StaticStateLike {
|
|
|
4
4
|
types: string[] | undefined
|
|
5
5
|
/** True: at most one item. False: may hold several. Undefined: cardinality unknown. */
|
|
6
6
|
single: boolean | undefined
|
|
7
|
+
/** True: ordered. False: unordered. Undefined: ordering is unknown. */
|
|
8
|
+
ordered: boolean | undefined
|
|
7
9
|
/** Canonical resource types a Reference state may point to — resolve()'s result. */
|
|
8
10
|
targets?: string[]
|
|
9
11
|
}
|
|
@@ -34,6 +36,8 @@ export type ValueArgSpec = 'any' | ValueKind
|
|
|
34
36
|
export interface InputSpec {
|
|
35
37
|
kind?: ValueKind
|
|
36
38
|
singleton?: boolean
|
|
39
|
+
/** True: the function needs an input with a defined order (`first()`, `skip()`). */
|
|
40
|
+
ordered?: boolean
|
|
37
41
|
/** Canonical or local model type names ('CodeableConcept', 'System.String'). */
|
|
38
42
|
types?: readonly string[]
|
|
39
43
|
}
|
|
@@ -47,7 +51,12 @@ export interface InputSpec {
|
|
|
47
51
|
export interface CustomFunctionSignature {
|
|
48
52
|
input?: InputSpec
|
|
49
53
|
args?: readonly ValueArgSpec[]
|
|
50
|
-
result?: {
|
|
54
|
+
result?: {
|
|
55
|
+
types?: readonly string[]
|
|
56
|
+
single?: boolean
|
|
57
|
+
/** True: ordered. False: unordered. Omit when the function does not declare ordering. */
|
|
58
|
+
ordered?: boolean
|
|
59
|
+
}
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
/**
|
|
@@ -56,14 +65,19 @@ export interface CustomFunctionSignature {
|
|
|
56
65
|
* implementations from acquiring separate handwritten function semantics.
|
|
57
66
|
*/
|
|
58
67
|
export type ResultRule =
|
|
59
|
-
| { kind: 'fixed'; types?: readonly string[]; single?: boolean }
|
|
60
|
-
|
|
68
|
+
| { kind: 'fixed'; types?: readonly string[]; single?: boolean; ordered?: boolean }
|
|
69
|
+
// `ordered: true` marks a function that establishes order (`sort()`).
|
|
70
|
+
| { kind: 'input'; ordered?: true }
|
|
61
71
|
| { kind: 'input-item' }
|
|
62
72
|
| { kind: 'argument'; index: number }
|
|
63
|
-
|
|
73
|
+
// `sequential: true` concatenates the sources (`union()`), so the result keeps
|
|
74
|
+
// order only when every source does; without it the sources are alternatives
|
|
75
|
+
// (`iif()` branches) and the union keeps only what they agree on.
|
|
76
|
+
| { kind: 'union'; sources: readonly ('input' | number)[]; single: boolean | 'all'; sequential?: true }
|
|
64
77
|
| { kind: 'arguments-union' }
|
|
65
78
|
| { kind: 'reference-targets' }
|
|
66
|
-
|
|
79
|
+
// An unknown result keeps the input's ordering unless the rule declares one.
|
|
80
|
+
| { kind: 'unknown'; ordered?: boolean }
|
|
67
81
|
|
|
68
82
|
export interface FunctionSignature {
|
|
69
83
|
input?: InputSpec
|
|
@@ -81,6 +95,11 @@ const DATETIME = { kind: 'fixed', types: ['System.DateTime'], single: true } as
|
|
|
81
95
|
const TIME = { kind: 'fixed', types: ['System.Time'], single: true } as const satisfies ResultRule
|
|
82
96
|
const QUANTITY = { kind: 'fixed', types: ['System.Quantity'], single: true } as const satisfies ResultRule
|
|
83
97
|
const UNKNOWN = { kind: 'unknown' } as const satisfies ResultRule
|
|
98
|
+
// An unknown type that is at most one item at runtime (aggregates, singleton-input
|
|
99
|
+
// conversions), so its order is defined even when the input's is not.
|
|
100
|
+
const UNKNOWN_ITEM = { kind: 'unknown', ordered: true } as const satisfies ResultRule
|
|
101
|
+
// Tree traversals return their matches in no defined order (spec §5.1).
|
|
102
|
+
const UNORDERED = { kind: 'unknown', ordered: false } as const satisfies ResultRule
|
|
84
103
|
const SAME = { kind: 'input' } as const satisfies ResultRule
|
|
85
104
|
const ITEM = { kind: 'input-item' } as const satisfies ResultRule
|
|
86
105
|
|
|
@@ -92,38 +111,75 @@ export function applyResultRule(
|
|
|
92
111
|
): StaticStateLike {
|
|
93
112
|
switch (rule.kind) {
|
|
94
113
|
case 'fixed':
|
|
95
|
-
return {
|
|
114
|
+
return {
|
|
115
|
+
types: rule.types === undefined ? undefined : [...rule.types],
|
|
116
|
+
single: rule.single,
|
|
117
|
+
ordered: singletonOrder(rule.single, rule.ordered),
|
|
118
|
+
}
|
|
96
119
|
case 'input':
|
|
97
|
-
return input
|
|
120
|
+
return rule.ordered === true ? withOrder(input, true) : input
|
|
98
121
|
case 'input-item':
|
|
99
122
|
return withSingle(input, true)
|
|
100
123
|
case 'argument': {
|
|
101
|
-
const argument = args[rule.index] ?? { types: undefined, single: undefined }
|
|
102
|
-
|
|
124
|
+
const argument = args[rule.index] ?? { types: undefined, single: undefined, ordered: undefined }
|
|
125
|
+
const result = withSingle(argument, singleAnd(input.single, argument.single))
|
|
126
|
+
return withOrder(result, sequentialOrder(input.ordered, argument.ordered))
|
|
103
127
|
}
|
|
104
128
|
case 'union': {
|
|
105
129
|
const states = rule.sources.map(source => (source === 'input' ? input : args[source]))
|
|
106
130
|
const merged = unionStates(states)
|
|
107
|
-
|
|
131
|
+
const result = rule.single === 'all' ? merged : withSingle(merged, rule.single)
|
|
132
|
+
if (rule.sequential !== true) {
|
|
133
|
+
return result
|
|
134
|
+
}
|
|
135
|
+
const present = states.filter((state): state is StaticStateLike => state !== undefined)
|
|
136
|
+
return withOrder(result, present.map(state => state.ordered).reduce(sequentialOrder, true))
|
|
108
137
|
}
|
|
109
138
|
case 'arguments-union':
|
|
110
139
|
return unionStates([...args])
|
|
111
140
|
case 'reference-targets':
|
|
112
|
-
return { types: input.targets, single: input.single }
|
|
141
|
+
return { types: input.targets, single: input.single, ordered: input.ordered }
|
|
113
142
|
case 'unknown':
|
|
114
|
-
return { types: undefined, single: undefined }
|
|
143
|
+
return { types: undefined, single: undefined, ordered: rule.ordered ?? input.ordered }
|
|
115
144
|
}
|
|
116
145
|
}
|
|
117
146
|
|
|
147
|
+
/** A collection known to hold at most one item is trivially ordered. */
|
|
148
|
+
export function singletonOrder(single: boolean | undefined, ordered: boolean | undefined): boolean | undefined {
|
|
149
|
+
return single === true ? true : ordered
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Exactly one item of the given types — the state every literal produces. */
|
|
153
|
+
export function singleState(types: string[] | undefined): StaticStateLike {
|
|
154
|
+
return { types, single: true, ordered: true }
|
|
155
|
+
}
|
|
156
|
+
|
|
118
157
|
/**
|
|
119
158
|
* The input's candidate types and reference targets at a different cardinality
|
|
120
159
|
* — the composition every selection/projection result goes through, so target
|
|
121
160
|
* metadata survives by construction instead of by per-function special cases.
|
|
122
161
|
*/
|
|
123
162
|
export function withSingle(input: StaticStateLike, single: boolean | undefined): StaticStateLike {
|
|
163
|
+
const ordered = singletonOrder(single, input.ordered)
|
|
164
|
+
return input.targets === undefined
|
|
165
|
+
? { types: input.types, single, ordered }
|
|
166
|
+
: { types: input.types, single, ordered, targets: input.targets }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Set ordering without losing type, cardinality, or Reference-target facts. */
|
|
170
|
+
export function withOrder(input: StaticStateLike, ordered: boolean | undefined): StaticStateLike {
|
|
171
|
+
const normalized = singletonOrder(input.single, ordered)
|
|
124
172
|
return input.targets === undefined
|
|
125
|
-
? { types: input.types, single }
|
|
126
|
-
: { types: input.types, single, targets: input.targets }
|
|
173
|
+
? { types: input.types, single: input.single, ordered: normalized }
|
|
174
|
+
: { types: input.types, single: input.single, ordered: normalized, targets: input.targets }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Flattening ordered subcollections preserves order only when both levels do. */
|
|
178
|
+
export function sequentialOrder(a: boolean | undefined, b: boolean | undefined): boolean | undefined {
|
|
179
|
+
if (a === false || b === false) {
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
182
|
+
return a === true && b === true ? true : undefined
|
|
127
183
|
}
|
|
128
184
|
|
|
129
185
|
/**
|
|
@@ -136,6 +192,14 @@ export function unionStates(states: (StaticStateLike | undefined)[]): StaticStat
|
|
|
136
192
|
const present = states.filter((state): state is StaticStateLike => state !== undefined)
|
|
137
193
|
const single = present.length > 0 ? present.map(state => state.single).reduce(singleAnd, true) : undefined
|
|
138
194
|
const contributing = present.filter(state => state.types === undefined || state.types.length > 0)
|
|
195
|
+
const ordered =
|
|
196
|
+
contributing.length === 0
|
|
197
|
+
? true
|
|
198
|
+
: contributing.every(state => state.ordered === true)
|
|
199
|
+
? true
|
|
200
|
+
: contributing.every(state => state.ordered === false)
|
|
201
|
+
? false
|
|
202
|
+
: undefined
|
|
139
203
|
const targets =
|
|
140
204
|
contributing.length > 0 && contributing.every(state => state.targets !== undefined)
|
|
141
205
|
? [...new Set(contributing.flatMap(state => state.targets as string[]))]
|
|
@@ -144,7 +208,7 @@ export function unionStates(states: (StaticStateLike | undefined)[]): StaticStat
|
|
|
144
208
|
present.length === 0 || present.some(state => state.types === undefined)
|
|
145
209
|
? undefined
|
|
146
210
|
: [...new Set(present.flatMap(state => state.types as string[]))]
|
|
147
|
-
return targets === undefined ? { types, single } : { types, single, targets }
|
|
211
|
+
return targets === undefined ? { types, single, ordered } : { types, single, ordered, targets }
|
|
148
212
|
}
|
|
149
213
|
|
|
150
214
|
const STRING_FN = {
|
|
@@ -183,22 +247,22 @@ const FUNCTION_SIGNATURE_DEFINITIONS = {
|
|
|
183
247
|
// the input and the projection body are single.
|
|
184
248
|
result: { kind: 'argument', index: 0 },
|
|
185
249
|
},
|
|
186
|
-
repeat: { args: ['expression'], result:
|
|
250
|
+
repeat: { args: ['expression'], result: UNORDERED },
|
|
187
251
|
// ofType/as results narrow to the named type; the analyzer computes that with
|
|
188
252
|
// the model (walkCall), so their table results are never consulted.
|
|
189
253
|
ofType: { args: ['type-name'], result: UNKNOWN },
|
|
190
254
|
is: { input: { singleton: true }, args: ['type-name'], result: BOOLEAN },
|
|
191
255
|
as: { input: { singleton: true }, args: ['type-name'], result: UNKNOWN },
|
|
192
256
|
single: { result: ITEM },
|
|
193
|
-
first: { result: ITEM },
|
|
194
|
-
last: { result: ITEM },
|
|
195
|
-
tail: { result: SAME },
|
|
196
|
-
skip: { args: ['Numeric'], result: SAME },
|
|
197
|
-
take: { args: ['Numeric'], result: SAME },
|
|
257
|
+
first: { input: { ordered: true }, result: ITEM },
|
|
258
|
+
last: { input: { ordered: true }, result: ITEM },
|
|
259
|
+
tail: { input: { ordered: true }, result: SAME },
|
|
260
|
+
skip: { input: { ordered: true }, args: ['Numeric'], result: SAME },
|
|
261
|
+
take: { input: { ordered: true }, args: ['Numeric'], result: SAME },
|
|
198
262
|
intersect: { args: ['any'], result: SAME },
|
|
199
263
|
exclude: { args: ['any'], result: SAME },
|
|
200
|
-
union: { args: ['any'], result: { kind: 'union', sources: ['input', 0], single: false } },
|
|
201
|
-
combine: { args: ['any'], result: { kind: 'union', sources: ['input', 0], single: false } },
|
|
264
|
+
union: { args: ['any'], result: { kind: 'union', sources: ['input', 0], single: false, sequential: true } },
|
|
265
|
+
combine: { args: ['any'], result: { kind: 'union', sources: ['input', 0], single: false, sequential: true } },
|
|
202
266
|
iif: {
|
|
203
267
|
args: ['condition', 'expression', 'expression'],
|
|
204
268
|
// The union of the branch states; a missing else-branch contributes empty.
|
|
@@ -207,14 +271,14 @@ const FUNCTION_SIGNATURE_DEFINITIONS = {
|
|
|
207
271
|
// not() takes anything a Boolean test accepts (0/1, single items), so no kind pin.
|
|
208
272
|
not: { input: { singleton: true }, result: BOOLEAN },
|
|
209
273
|
trace: { args: ['String', 'expression'], result: SAME },
|
|
210
|
-
children: { result:
|
|
211
|
-
descendants: { result:
|
|
274
|
+
children: { result: UNORDERED },
|
|
275
|
+
descendants: { result: UNORDERED },
|
|
212
276
|
// A reference resolves to its declared target types (Reference.targetProfile,
|
|
213
277
|
// HAPI's TypeDetails.targets); an unconstrained reference stays unknown.
|
|
214
278
|
resolve: { result: { kind: 'reference-targets' } },
|
|
215
279
|
extension: { args: ['String'], result: UNKNOWN },
|
|
216
280
|
hasValue: { input: { singleton: true }, result: BOOLEAN },
|
|
217
|
-
getValue: { input: { singleton: true }, result:
|
|
281
|
+
getValue: { input: { singleton: true }, result: UNKNOWN_ITEM },
|
|
218
282
|
htmlChecks: { input: { singleton: true }, result: BOOLEAN },
|
|
219
283
|
comparable: { input: { kind: 'Quantity', singleton: true }, args: ['Quantity'], result: BOOLEAN },
|
|
220
284
|
conformsTo: { input: { singleton: true }, args: ['String'], result: BOOLEAN },
|
|
@@ -234,13 +298,13 @@ const FUNCTION_SIGNATURE_DEFINITIONS = {
|
|
|
234
298
|
replaceMatches: { input: { kind: 'String', singleton: true }, args: ['String', 'String'], result: STRING },
|
|
235
299
|
toChars: {
|
|
236
300
|
input: { kind: 'String', singleton: true },
|
|
237
|
-
result: { kind: 'fixed', types: ['System.String'], single: false },
|
|
301
|
+
result: { kind: 'fixed', types: ['System.String'], single: false, ordered: true },
|
|
238
302
|
},
|
|
239
303
|
trim: { input: { kind: 'String', singleton: true }, result: STRING },
|
|
240
304
|
split: {
|
|
241
305
|
input: { kind: 'String', singleton: true },
|
|
242
306
|
args: ['String'],
|
|
243
|
-
result: { kind: 'fixed', types: ['System.String'], single: false },
|
|
307
|
+
result: { kind: 'fixed', types: ['System.String'], single: false, ordered: true },
|
|
244
308
|
},
|
|
245
309
|
join: { input: { kind: 'String' }, args: ['String'], result: STRING },
|
|
246
310
|
encode: STRING_FN,
|
|
@@ -248,7 +312,7 @@ const FUNCTION_SIGNATURE_DEFINITIONS = {
|
|
|
248
312
|
escape: STRING_FN,
|
|
249
313
|
unescape: STRING_FN,
|
|
250
314
|
|
|
251
|
-
abs: { input: { singleton: true }, result:
|
|
315
|
+
abs: { input: { singleton: true }, result: UNKNOWN_ITEM },
|
|
252
316
|
ceiling: { input: { kind: 'Numeric', singleton: true }, result: INTEGER },
|
|
253
317
|
floor: { input: { kind: 'Numeric', singleton: true }, result: INTEGER },
|
|
254
318
|
truncate: { input: { kind: 'Numeric', singleton: true }, result: INTEGER },
|
|
@@ -257,15 +321,15 @@ const FUNCTION_SIGNATURE_DEFINITIONS = {
|
|
|
257
321
|
ln: MATH_FN,
|
|
258
322
|
sqrt: MATH_FN,
|
|
259
323
|
log: { input: { kind: 'Numeric', singleton: true }, args: ['Numeric'], result: DECIMAL },
|
|
260
|
-
power: { input: { kind: 'Numeric', singleton: true }, args: ['Numeric'], result:
|
|
324
|
+
power: { input: { kind: 'Numeric', singleton: true }, args: ['Numeric'], result: UNKNOWN_ITEM },
|
|
261
325
|
// Each iteration replaces the accumulator with the aggregator result. An
|
|
262
326
|
// empty input returns init, when supplied, so both arguments can contribute.
|
|
263
327
|
aggregate: { args: ['expression', 'any'], result: { kind: 'union', sources: [0, 1], single: 'all' } },
|
|
264
|
-
sum: { input: { kind: 'Numeric' }, result:
|
|
265
|
-
min: { input: { kind: 'Numeric' }, result:
|
|
266
|
-
max: { input: { kind: 'Numeric' }, result:
|
|
328
|
+
sum: { input: { kind: 'Numeric' }, result: UNKNOWN_ITEM },
|
|
329
|
+
min: { input: { kind: 'Numeric' }, result: UNKNOWN_ITEM },
|
|
330
|
+
max: { input: { kind: 'Numeric' }, result: UNKNOWN_ITEM },
|
|
267
331
|
avg: { input: { kind: 'Numeric' }, result: DECIMAL },
|
|
268
|
-
sort: { args: ['sort-key'], result:
|
|
332
|
+
sort: { args: ['sort-key'], result: { kind: 'input', ordered: true } },
|
|
269
333
|
|
|
270
334
|
toBoolean: { input: { singleton: true }, result: BOOLEAN },
|
|
271
335
|
toInteger: { input: { singleton: true }, result: INTEGER },
|
|
@@ -303,8 +367,8 @@ const FUNCTION_SIGNATURE_DEFINITIONS = {
|
|
|
303
367
|
timezoneOffsetOf: { input: { kind: 'Temporal', singleton: true }, result: DECIMAL },
|
|
304
368
|
dateOf: { input: { kind: 'Temporal', singleton: true }, result: DATE },
|
|
305
369
|
timeOf: { input: { kind: 'Temporal', singleton: true }, result: TIME },
|
|
306
|
-
lowBoundary: { input: { singleton: true }, args: ['Numeric'], result:
|
|
307
|
-
highBoundary: { input: { singleton: true }, args: ['Numeric'], result:
|
|
370
|
+
lowBoundary: { input: { singleton: true }, args: ['Numeric'], result: UNKNOWN_ITEM },
|
|
371
|
+
highBoundary: { input: { singleton: true }, args: ['Numeric'], result: UNKNOWN_ITEM },
|
|
308
372
|
precision: { input: { singleton: true }, result: INTEGER },
|
|
309
373
|
defineVariable: { args: ['String', 'expression'], result: SAME },
|
|
310
374
|
// Variadic: the analyzer repeats the last arg spec for every position, so one
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { AnalyzerVariable } from './declarations.ts'
|
|
2
|
+
|
|
3
|
+
/** Internal source-analysis metadata. A symbol keeps it out of the public AnalyzeOptions contract. */
|
|
4
|
+
export const SOURCE_VARIABLE_DEFAULTS = Symbol('sourceVariableDefaults')
|
|
5
|
+
|
|
6
|
+
export interface SourceVariableDefaults {
|
|
7
|
+
/** Final normalized engine-default vars value names in Object.entries order. */
|
|
8
|
+
values: readonly string[]
|
|
9
|
+
/** Engine-default varTypes declarations. */
|
|
10
|
+
declarations: Readonly<Record<string, AnalyzerVariable>>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Ordered vars metadata shared by source extraction and loaded-context analysis. */
|
|
14
|
+
export interface SourceVariablePlan {
|
|
15
|
+
values: readonly string[]
|
|
16
|
+
declarations: Readonly<Record<string, AnalyzerVariable>>
|
|
17
|
+
inheritsDeclarations: boolean
|
|
18
|
+
before?: string
|
|
19
|
+
}
|
package/src/api/dto.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { mergeEnvKeys, normalizeEnvKeys } from '../engine/context.ts'
|
|
|
4
4
|
import { FhirPathTypeError } from '../errors.ts'
|
|
5
5
|
import { functions as builtinFunctions } from '../functions/registry.ts'
|
|
6
6
|
import type { ModelProvider } from '../model/provider.ts'
|
|
7
|
-
import type { FhirTypeName } from '../typed/infer.ts'
|
|
7
|
+
import type { FhirpathTypeDeclarations, FhirTypeName } from '../typed/infer.ts'
|
|
8
8
|
import { canonicalFocusType, typesOverlap } from '../values/type-compat.ts'
|
|
9
9
|
import type { TypedValue } from '../values/typed-value.ts'
|
|
10
10
|
import { toSubjects } from './bundle.ts'
|
|
@@ -80,8 +80,11 @@ export type DtoRow<C extends DtoClass> = InstanceType<C>
|
|
|
80
80
|
export interface DtoOptions {
|
|
81
81
|
/** Per-row bindings the columns read (EvaluateOptions.vars semantics; may reference per-call env). */
|
|
82
82
|
vars?: Record<string, AnyExpression | readonly TypedValue[]>
|
|
83
|
-
/**
|
|
84
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Environment supplied by `project()`. Use names when only presence is known,
|
|
85
|
+
* or declarations when DTO vars navigate through those values.
|
|
86
|
+
*/
|
|
87
|
+
callerEnv?: readonly string[] | FhirpathTypeDeclarations
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
/** Everything a DTO class was declared with; `project()`, the engine, and `analyzeDto` all read it from here. */
|
|
@@ -93,11 +96,20 @@ export interface DtoDefinition {
|
|
|
93
96
|
readonly env: Record<string, unknown> | undefined
|
|
94
97
|
readonly vars: Record<string, AnyExpression | readonly TypedValue[]> | undefined
|
|
95
98
|
/** Env names the projecting call supplies (see DtoOptions.callerEnv). */
|
|
96
|
-
readonly
|
|
99
|
+
readonly callerEnvNames: readonly string[]
|
|
100
|
+
/** Static types for the caller environment, when declared. */
|
|
101
|
+
readonly callerEnvTypes: FhirpathTypeDeclarations | undefined
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
interface DtoBaseDefinition {
|
|
105
|
+
fhirType: string
|
|
106
|
+
vars: DtoOptions['vars']
|
|
107
|
+
callerEnvNames: readonly string[]
|
|
108
|
+
callerEnvTypes: FhirpathTypeDeclarations | undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The normalized options a `defineDto()` base was created with. */
|
|
112
|
+
const bases = new WeakMap<object, DtoBaseDefinition>()
|
|
101
113
|
|
|
102
114
|
/**
|
|
103
115
|
* Columns by the class that declared them. A field decorator runs before its
|
|
@@ -128,10 +140,23 @@ export function defineDto<const Root extends FhirTypeName>(fhirType: Root, optio
|
|
|
128
140
|
// A readable name for project()/registration errors; a subclass replaces it.
|
|
129
141
|
Object.defineProperty(base, 'name', { value: `${fhirType}Dto` })
|
|
130
142
|
Object.defineProperty(base, 'fhirType', { value: fhirType, enumerable: true })
|
|
131
|
-
|
|
143
|
+
const callerEnv = options.callerEnv
|
|
144
|
+
const callerEnvIsNames = isCallerEnvNames(callerEnv)
|
|
145
|
+
bases.set(base, {
|
|
146
|
+
fhirType,
|
|
147
|
+
vars: options.vars,
|
|
148
|
+
callerEnvNames: callerEnvIsNames ? callerEnv : Object.keys(callerEnv ?? {}),
|
|
149
|
+
callerEnvTypes: callerEnvIsNames ? undefined : callerEnv,
|
|
150
|
+
})
|
|
132
151
|
return base as unknown as DtoBase<Root>
|
|
133
152
|
}
|
|
134
153
|
|
|
154
|
+
function isCallerEnvNames(
|
|
155
|
+
callerEnv: readonly string[] | FhirpathTypeDeclarations | undefined
|
|
156
|
+
): callerEnv is readonly string[] {
|
|
157
|
+
return Array.isArray(callerEnv)
|
|
158
|
+
}
|
|
159
|
+
|
|
135
160
|
/** Records one column against the class being collected, and does nothing at any other time. */
|
|
136
161
|
function recordColumn(instance: object, name: string, spec: ColumnSpec): void {
|
|
137
162
|
const cls = instance.constructor as object
|
|
@@ -191,7 +216,7 @@ function* classChain(cls: object): Generator<{ readonly name: string }> {
|
|
|
191
216
|
}
|
|
192
217
|
|
|
193
218
|
/** The `defineDto()` base a class descends from, with the fhirType/vars it fixed. */
|
|
194
|
-
function baseOf(cls: object):
|
|
219
|
+
function baseOf(cls: object): DtoBaseDefinition | undefined {
|
|
195
220
|
for (const current of classChain(cls)) {
|
|
196
221
|
const base = bases.get(current)
|
|
197
222
|
if (base !== undefined) {
|
|
@@ -286,7 +311,8 @@ export function dtoDefinition(cls: DtoClass): DtoDefinition {
|
|
|
286
311
|
columns,
|
|
287
312
|
env: declaredEnv(cls),
|
|
288
313
|
vars: base.vars,
|
|
289
|
-
|
|
314
|
+
callerEnvNames: base.callerEnvNames,
|
|
315
|
+
callerEnvTypes: base.callerEnvTypes,
|
|
290
316
|
}
|
|
291
317
|
definitions.set(cls, definition)
|
|
292
318
|
return definition
|
package/src/api/strict.ts
CHANGED
|
@@ -52,13 +52,17 @@ export function assertStrictExpression(ast: AstNode, root: TypedValue[], options
|
|
|
52
52
|
.filter(diagnostic => diagnostic.severity === 'error')
|
|
53
53
|
.map(diagnostic => ({ diagnostic, subject: `vars.${name}` }))
|
|
54
54
|
)
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
// A declaration overrides the inferred types and cardinality, but ordering
|
|
56
|
+
// always comes from the analyzed expression — declarations cannot state it.
|
|
57
|
+
variables[name] = {
|
|
58
|
+
...(declaration === undefined
|
|
57
59
|
? {
|
|
58
60
|
...(details.result.types !== undefined && { types: details.result.types }),
|
|
59
61
|
...(details.result.single !== undefined && { single: details.result.single }),
|
|
60
62
|
}
|
|
61
|
-
: analyzerVariable(declaration)
|
|
63
|
+
: analyzerVariable(declaration)),
|
|
64
|
+
...(details.result.ordered !== undefined && { ordered: details.result.ordered }),
|
|
65
|
+
}
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
for (const [name, declaration] of Object.entries(declarations)) {
|
|
@@ -91,6 +95,7 @@ function runtimeRoot(root: TypedValue[], model: ModelProvider | undefined): Anal
|
|
|
91
95
|
return {
|
|
92
96
|
types: variable.types,
|
|
93
97
|
single: variable.single,
|
|
98
|
+
ordered: variable.ordered,
|
|
94
99
|
exactTypes: variable.exactTypes,
|
|
95
100
|
}
|
|
96
101
|
}
|
package/src/cli/dto-check.ts
CHANGED
|
@@ -9,13 +9,22 @@ import { register } from 'node:module'
|
|
|
9
9
|
import { relative } from 'node:path'
|
|
10
10
|
import { pathToFileURL } from 'node:url'
|
|
11
11
|
|
|
12
|
+
import type { AnalyzeOptions } from '../analyzer/analyze.ts'
|
|
12
13
|
import { type AnalyzedContext, analyzeDto, type DtoDiagnostic } from '../analyzer/analyze-dto.ts'
|
|
14
|
+
import { analyzerEnvironmentVariables, analyzerVariables } from '../analyzer/declarations.ts'
|
|
15
|
+
import { SOURCE_VARIABLE_DEFAULTS } from '../analyzer/source-options.ts'
|
|
13
16
|
import { type DtoClass, isDtoClass } from '../api/dto.ts'
|
|
14
17
|
import { type FhirPathEngine, recordEngines } from '../api/engine.ts'
|
|
15
18
|
|
|
16
19
|
/** Where DTO classes live unless `--dtos` says otherwise. */
|
|
17
20
|
export const DEFAULT_DTO_GLOB = '**/*.dto.ts'
|
|
18
21
|
|
|
22
|
+
/**
|
|
23
|
+
* A checker configuration problem, not an expression finding: the imported
|
|
24
|
+
* modules constructed engines whose contexts cannot be merged.
|
|
25
|
+
*/
|
|
26
|
+
export class EngineMergeError extends Error {}
|
|
27
|
+
|
|
19
28
|
const IGNORED = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/coverage/**']
|
|
20
29
|
|
|
21
30
|
/** One finding, with the DTO and file it belongs to and the expression that produced it. */
|
|
@@ -34,6 +43,8 @@ export interface DtoCheckResult {
|
|
|
34
43
|
engines: number
|
|
35
44
|
/** Every DTO analyzed, by module. */
|
|
36
45
|
dtos: { file: string; dto: string }[]
|
|
46
|
+
/** Merged engine declarations that make ordinary source-site checks complete. */
|
|
47
|
+
sourceOptions: AnalyzeOptions | undefined
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
/** Imports matching modules and checks their exported DTOs against recorded engines. */
|
|
@@ -53,10 +64,17 @@ export async function checkDtoModules(patterns: readonly string[], cwd: string):
|
|
|
53
64
|
}
|
|
54
65
|
}
|
|
55
66
|
const engines = recorded()
|
|
67
|
+
assertSharedModel(engines)
|
|
56
68
|
const findings = dtos.flatMap(({ file, dto, cls }) =>
|
|
57
69
|
analyzeFor(cls, engines).map(finding => ({ ...finding, dto, file }))
|
|
58
70
|
)
|
|
59
|
-
return {
|
|
71
|
+
return {
|
|
72
|
+
findings,
|
|
73
|
+
files,
|
|
74
|
+
engines: engines.length,
|
|
75
|
+
dtos: dtos.map(({ file, dto }) => ({ file, dto })),
|
|
76
|
+
sourceOptions: engines.length === 0 ? undefined : optionsForSource(merged(engines)),
|
|
77
|
+
}
|
|
60
78
|
}
|
|
61
79
|
|
|
62
80
|
/**
|
|
@@ -70,19 +88,36 @@ export async function checkDtoModules(patterns: readonly string[], cwd: string):
|
|
|
70
88
|
function analyzeFor(dto: DtoClass, engines: readonly FhirPathEngine[]): DtoDiagnostic[] {
|
|
71
89
|
const owner = engines.find(engine => engine.dtos.includes(dto))
|
|
72
90
|
if (owner !== undefined) {
|
|
73
|
-
return analyzeDto(dto, { engine: owner })
|
|
91
|
+
return analyzeDto(dto, { engine: owner, reportUnchecked: true })
|
|
74
92
|
}
|
|
75
93
|
if (engines.length === 0) {
|
|
76
|
-
return analyzeDto(dto)
|
|
94
|
+
return analyzeDto(dto, { reportUnchecked: true })
|
|
95
|
+
}
|
|
96
|
+
return analyzeDto(dto, { engine: merged(engines), reportUnchecked: true })
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Merged analysis (unregistered DTOs, `sourceOptions`) uses the first engine's
|
|
101
|
+
* model for every declaration, so all engines must share one `ModelProvider`
|
|
102
|
+
* instance — a declaration analyzed under another engine's type hierarchy would
|
|
103
|
+
* produce wrong element, subtype, and Reference-target findings. Identity is
|
|
104
|
+
* the only equivalence a `ModelProvider` offers, so two wrappers around the
|
|
105
|
+
* same logical model are still rejected; check such projects in separate runs.
|
|
106
|
+
*/
|
|
107
|
+
function assertSharedModel(engines: readonly FhirPathEngine[]): void {
|
|
108
|
+
const model = engines[0]?.defaults.model
|
|
109
|
+
if (engines.some(engine => engine.defaults.model !== model)) {
|
|
110
|
+
throw new EngineMergeError(
|
|
111
|
+
'the imported modules constructed engines with different ModelProvider instances; ' +
|
|
112
|
+
'source and unregistered-DTO analysis needs one shared model — check projects with different models in separate runs'
|
|
113
|
+
)
|
|
77
114
|
}
|
|
78
|
-
return analyzeDto(dto, { engine: merged(engines) })
|
|
79
115
|
}
|
|
80
116
|
|
|
81
117
|
/**
|
|
82
118
|
* Every engine's context as one: the union of their registered functions,
|
|
83
119
|
* environment declarations, and vars. The first engine's `model` stands for
|
|
84
|
-
* all of them —
|
|
85
|
-
* no engine that could pick another.
|
|
120
|
+
* all of them — `assertSharedModel` has proven they all carry the same one.
|
|
86
121
|
*/
|
|
87
122
|
function merged(engines: readonly FhirPathEngine[]): AnalyzedContext {
|
|
88
123
|
return {
|
|
@@ -96,3 +131,18 @@ function merged(engines: readonly FhirPathEngine[]): AnalyzedContext {
|
|
|
96
131
|
},
|
|
97
132
|
}
|
|
98
133
|
}
|
|
134
|
+
|
|
135
|
+
/** Turn merged engine runtime defaults into the declarations accepted by analyzeSite(). */
|
|
136
|
+
function optionsForSource(engine: AnalyzedContext): AnalyzeOptions {
|
|
137
|
+
const { model, functions, env, envTypes, vars, varTypes } = engine.defaults
|
|
138
|
+
const options = {
|
|
139
|
+
...(model !== undefined && { model }),
|
|
140
|
+
...(functions !== undefined && { functions }),
|
|
141
|
+
variables: analyzerEnvironmentVariables(env, envTypes, model),
|
|
142
|
+
[SOURCE_VARIABLE_DEFAULTS]: {
|
|
143
|
+
values: Object.keys(analyzerVariables(vars, undefined)),
|
|
144
|
+
declarations: analyzerVariables(undefined, varTypes),
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
return options
|
|
148
|
+
}
|