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
package/src/analyzer/analyze.ts
CHANGED
|
@@ -34,9 +34,12 @@ import {
|
|
|
34
34
|
type FunctionSignature,
|
|
35
35
|
type InputSpec,
|
|
36
36
|
singleAnd,
|
|
37
|
+
singleState,
|
|
38
|
+
singletonOrder,
|
|
37
39
|
unionStates,
|
|
38
40
|
withSingle,
|
|
39
41
|
} from './signatures.ts'
|
|
42
|
+
import { SOURCE_VARIABLE_DEFAULTS, type SourceVariableDefaults, type SourceVariablePlan } from './source-options.ts'
|
|
40
43
|
|
|
41
44
|
export interface AnalyzerDiagnostic {
|
|
42
45
|
severity: 'error' | 'warning'
|
|
@@ -91,6 +94,8 @@ export interface AnalyzeOptions {
|
|
|
91
94
|
functions?: Record<string, DeclaredFunction>
|
|
92
95
|
/** Host-supplied environment variables by name (with or without the leading `%`). */
|
|
93
96
|
variables?: Record<string, DeclaredVariable>
|
|
97
|
+
/** Report navigation that remains unchecked because a declared host variable has no type. */
|
|
98
|
+
reportUnchecked?: boolean
|
|
94
99
|
}
|
|
95
100
|
|
|
96
101
|
/** Internal strict-analysis options whose variables may retain exact runtime focus types. */
|
|
@@ -106,6 +111,8 @@ export interface RuntimeAnalyzeOptions extends Omit<AnalyzeOptions, 'variables'>
|
|
|
106
111
|
interface StaticState {
|
|
107
112
|
types: string[] | undefined
|
|
108
113
|
single: boolean | undefined
|
|
114
|
+
/** True: ordered. False: unordered. Undefined: ordering is unknown. */
|
|
115
|
+
ordered: boolean | undefined
|
|
109
116
|
/** Exact runtime focus types, present only while strict analysis can still prove them. */
|
|
110
117
|
exactTypes?: string[]
|
|
111
118
|
/**
|
|
@@ -116,16 +123,22 @@ interface StaticState {
|
|
|
116
123
|
targets?: string[]
|
|
117
124
|
/** True until the raw input has been narrowed or navigated through the model. */
|
|
118
125
|
rawInput?: boolean
|
|
126
|
+
/** Untyped host variable whose first element navigation cannot be checked. */
|
|
127
|
+
opaqueVariable?: string
|
|
119
128
|
}
|
|
120
129
|
|
|
121
|
-
const UNKNOWN: StaticState = { types: undefined, single: undefined }
|
|
130
|
+
const UNKNOWN: StaticState = { types: undefined, single: undefined, ordered: undefined }
|
|
122
131
|
|
|
123
132
|
/** A runtime-analyzed function result no longer has exact facts about its input focus. */
|
|
124
|
-
function callResult(
|
|
133
|
+
function callResult(
|
|
134
|
+
state: Pick<StaticState, 'types' | 'single' | 'ordered' | 'targets'>,
|
|
135
|
+
runtime: boolean
|
|
136
|
+
): StaticState {
|
|
125
137
|
return runtime
|
|
126
138
|
? {
|
|
127
139
|
types: state.types,
|
|
128
140
|
single: state.single,
|
|
141
|
+
ordered: state.ordered,
|
|
129
142
|
...(state.targets !== undefined && { targets: state.targets }),
|
|
130
143
|
}
|
|
131
144
|
: state
|
|
@@ -152,14 +165,14 @@ function forkScope(scope: VariableScope): VariableScope {
|
|
|
152
165
|
export interface AnalysisDetails {
|
|
153
166
|
diagnostics: AnalyzerDiagnostic[]
|
|
154
167
|
/**
|
|
155
|
-
* The expression's inferred result: canonical type names
|
|
156
|
-
* same StaticState the checks run on. `types: undefined` means
|
|
157
|
-
* cannot see through the expression (an unknown region), and
|
|
158
|
-
* undefined means
|
|
159
|
-
* caller cross-check a declared type against what the
|
|
160
|
-
* yields (see `analyzeDto`).
|
|
168
|
+
* The expression's inferred result: canonical type names, cardinality, and
|
|
169
|
+
* ordering, the same StaticState the checks run on. `types: undefined` means
|
|
170
|
+
* the analyzer cannot see through the expression (an unknown region), and an
|
|
171
|
+
* undefined `single` or `ordered` means that fact is unknown — none of these
|
|
172
|
+
* is an error. Lets a caller cross-check a declared type against what the
|
|
173
|
+
* expression really yields (see `analyzeDto`).
|
|
161
174
|
*/
|
|
162
|
-
result: { types: string[] | undefined; single: boolean | undefined }
|
|
175
|
+
result: { types: string[] | undefined; single: boolean | undefined; ordered: boolean | undefined }
|
|
163
176
|
/**
|
|
164
177
|
* `Type.element` paths the expression reads (local type names), deduped in
|
|
165
178
|
* first-visit order — HAPI's `elementDependencies`. Lets callers know which
|
|
@@ -172,6 +185,7 @@ export interface AnalysisDetails {
|
|
|
172
185
|
export interface AnalyzerRoot {
|
|
173
186
|
types: string[] | undefined
|
|
174
187
|
single: boolean | undefined
|
|
188
|
+
ordered: boolean | undefined
|
|
175
189
|
/** Canonical runtime types in focus order; an empty array means an exactly empty focus. */
|
|
176
190
|
exactTypes?: string[]
|
|
177
191
|
}
|
|
@@ -188,32 +202,63 @@ export function analyzeExpression(expression: string, options?: AnalyzeOptions):
|
|
|
188
202
|
|
|
189
203
|
/**
|
|
190
204
|
* Analyzes one source site with only facts visible in that file. A declared root
|
|
191
|
-
* does not prove which variables the later call provides.
|
|
192
|
-
*
|
|
193
|
-
* `
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
205
|
+
* does not prove which variables the later call provides. A site whose call
|
|
206
|
+
* binds variables the source cannot name (`openVariables`) may resolve any
|
|
207
|
+
* `%variable` at runtime, so an unresolved one is reported as an
|
|
208
|
+
* `unchecked-variable` warning under `reportUnchecked` instead of an error.
|
|
209
|
+
* DTO sites may also receive variables and functions from base classes, other
|
|
210
|
+
* modules, or `project()`. Such sites omit unknown-variable diagnostics and
|
|
211
|
+
* report an unknown function only when it resembles a local column. A DTO
|
|
212
|
+
* without a known root receives syntax checks only. Use `analyzeDto` when the
|
|
213
|
+
* class and engine are loaded.
|
|
197
214
|
*/
|
|
198
215
|
export function analyzeSite(
|
|
199
216
|
site: {
|
|
200
217
|
expression: string
|
|
201
218
|
inputType?: string
|
|
202
219
|
dto?: true
|
|
220
|
+
/** Variables declared by the expression's call site, such as inline env/vars options. */
|
|
221
|
+
variables?: Readonly<Record<string, DeclaredVariable>>
|
|
222
|
+
/** The call also binds variables the source cannot name — a computed key or spread in env/vars. */
|
|
223
|
+
openVariables?: true
|
|
203
224
|
/** Functions the site's file declares — a DTO's `@column` fields (see `columnFunctionDeclaration`). */
|
|
204
225
|
functions?: Readonly<Record<string, DeclaredFunction>>
|
|
205
226
|
},
|
|
206
227
|
options?: AnalyzeOptions
|
|
207
228
|
): AnalyzerDiagnostic[] {
|
|
229
|
+
const sourceVariables = (options as { [SOURCE_VARIABLE_DEFAULTS]?: SourceVariableDefaults } | undefined)?.[
|
|
230
|
+
SOURCE_VARIABLE_DEFAULTS
|
|
231
|
+
]
|
|
232
|
+
const variablePlan = (site as typeof site & { variablePlan?: SourceVariablePlan }).variablePlan
|
|
208
233
|
const declared = { ...site.functions, ...options?.functions }
|
|
234
|
+
const variables = sourceSiteVariables(site.variables, variablePlan, options?.variables, sourceVariables)
|
|
209
235
|
const merged: AnalyzeOptions = {
|
|
210
236
|
...options,
|
|
211
237
|
...(site.inputType !== undefined && { inputType: site.inputType }),
|
|
212
238
|
...(Object.keys(declared).length > 0 && { functions: declared }),
|
|
239
|
+
...(variables !== undefined && { variables }),
|
|
213
240
|
}
|
|
214
241
|
const diagnostics = analyzeExpression(site.expression, merged)
|
|
215
242
|
if (site.dto !== true) {
|
|
216
|
-
|
|
243
|
+
if (site.openVariables === true) {
|
|
244
|
+
return diagnostics.flatMap(diagnostic => {
|
|
245
|
+
if (diagnostic.code !== 'unknown-variable') {
|
|
246
|
+
return [diagnostic]
|
|
247
|
+
}
|
|
248
|
+
if (options?.reportUnchecked !== true) {
|
|
249
|
+
return []
|
|
250
|
+
}
|
|
251
|
+
return [
|
|
252
|
+
{
|
|
253
|
+
...diagnostic,
|
|
254
|
+
severity: 'warning' as const,
|
|
255
|
+
code: 'unchecked-variable',
|
|
256
|
+
message: `Environment variable %${diagnostic.name} was not checked: the call's env/vars names are not fully visible in source`,
|
|
257
|
+
},
|
|
258
|
+
]
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
return site.inputType === undefined || options?.variables !== undefined
|
|
217
262
|
? diagnostics
|
|
218
263
|
: diagnostics.filter(diagnostic => diagnostic.code !== 'unknown-variable')
|
|
219
264
|
}
|
|
@@ -232,6 +277,42 @@ export function analyzeSite(
|
|
|
232
277
|
})
|
|
233
278
|
}
|
|
234
279
|
|
|
280
|
+
/** Recreate strict evaluation's vars prefix without flattening away engine/default insertion order. */
|
|
281
|
+
function sourceSiteVariables(
|
|
282
|
+
siteEnvironment: Readonly<Record<string, DeclaredVariable>> | undefined,
|
|
283
|
+
plan: SourceVariablePlan | undefined,
|
|
284
|
+
engineEnvironment: Readonly<Record<string, DeclaredVariable>> | undefined,
|
|
285
|
+
defaults: SourceVariableDefaults | undefined
|
|
286
|
+
): Record<string, DeclaredVariable> | undefined {
|
|
287
|
+
if (
|
|
288
|
+
siteEnvironment === undefined &&
|
|
289
|
+
engineEnvironment === undefined &&
|
|
290
|
+
defaults === undefined &&
|
|
291
|
+
plan === undefined
|
|
292
|
+
) {
|
|
293
|
+
return undefined
|
|
294
|
+
}
|
|
295
|
+
const variables: Record<string, DeclaredVariable> = { ...engineEnvironment, ...siteEnvironment }
|
|
296
|
+
const declarations =
|
|
297
|
+
plan?.inheritsDeclarations === false ? plan.declarations : { ...defaults?.declarations, ...plan?.declarations }
|
|
298
|
+
const valueNames = [...(defaults?.values ?? [])]
|
|
299
|
+
for (const name of plan?.values ?? []) {
|
|
300
|
+
if (!valueNames.includes(name)) {
|
|
301
|
+
valueNames.push(name)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const end = plan?.before === undefined ? valueNames.length : valueNames.indexOf(plan.before)
|
|
305
|
+
for (const name of valueNames.slice(0, end < 0 ? valueNames.length : end)) {
|
|
306
|
+
variables[name] = declarations[name] ?? {}
|
|
307
|
+
}
|
|
308
|
+
if (plan?.before === undefined) {
|
|
309
|
+
for (const [name, declaration] of Object.entries(declarations)) {
|
|
310
|
+
variables[name] ??= declaration
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return variables
|
|
314
|
+
}
|
|
315
|
+
|
|
235
316
|
/** `analyzeExpression` plus the element paths the expression touches and its inferred result type. */
|
|
236
317
|
export function analyzeExpressionDetailed(expression: string, options?: AnalyzeOptions): AnalysisDetails {
|
|
237
318
|
let ast: AstNode
|
|
@@ -246,7 +327,7 @@ export function analyzeExpressionDetailed(expression: string, options?: AnalyzeO
|
|
|
246
327
|
return {
|
|
247
328
|
diagnostics: [{ severity: 'error', code: 'syntax', message: error.message, span: error.span }],
|
|
248
329
|
elementDependencies: [],
|
|
249
|
-
result: { types: undefined, single: undefined },
|
|
330
|
+
result: { types: undefined, single: undefined, ordered: undefined },
|
|
250
331
|
}
|
|
251
332
|
}
|
|
252
333
|
return analyzeAstDetailed(ast, options)
|
|
@@ -263,7 +344,7 @@ function analyzeAstDetailed(
|
|
|
263
344
|
return {
|
|
264
345
|
diagnostics: analyzer.diagnostics,
|
|
265
346
|
elementDependencies: [...analyzer.dependencies],
|
|
266
|
-
result: { types: state.types, single: state.single },
|
|
347
|
+
result: { types: state.types, single: state.single, ordered: state.ordered },
|
|
267
348
|
}
|
|
268
349
|
}
|
|
269
350
|
|
|
@@ -287,16 +368,20 @@ class Analyzer {
|
|
|
287
368
|
private readonly customFunctions: ReadonlyMap<string, readonly ResolvedDeclaration[]>
|
|
288
369
|
private readonly declaredVariables: ReadonlyMap<string, AnalyzerVariableState>
|
|
289
370
|
private readonly activeExpressionFunctions = new Set<string>()
|
|
371
|
+
private readonly reportUnchecked: boolean
|
|
290
372
|
|
|
291
373
|
constructor(options: AnalyzeOptions | RuntimeAnalyzeOptions | undefined, root: AnalyzerRoot | undefined) {
|
|
292
374
|
this.model = options?.model
|
|
293
375
|
this.runtime = root !== undefined
|
|
376
|
+
this.reportUnchecked = options?.reportUnchecked === true
|
|
294
377
|
const inputType = options?.inputType
|
|
378
|
+
// The root focus is a real collection, so its order is defined even when
|
|
379
|
+
// its types and cardinality are not.
|
|
295
380
|
this.root =
|
|
296
381
|
root ??
|
|
297
382
|
(inputType === undefined
|
|
298
|
-
? { types: undefined, single: undefined }
|
|
299
|
-
:
|
|
383
|
+
? { types: undefined, single: undefined, ordered: true }
|
|
384
|
+
: singleState([this.model?.resolveType(inputType) ?? inputType]))
|
|
300
385
|
this.customFunctions = new Map(
|
|
301
386
|
Object.entries(options?.functions ?? {}).map(([name, declared]) => [
|
|
302
387
|
name,
|
|
@@ -312,6 +397,7 @@ class Analyzer {
|
|
|
312
397
|
return {
|
|
313
398
|
types: this.root.types,
|
|
314
399
|
single: this.root.single,
|
|
400
|
+
ordered: this.root.ordered,
|
|
315
401
|
rawInput: true,
|
|
316
402
|
...(this.root.exactTypes !== undefined && { exactTypes: this.root.exactTypes }),
|
|
317
403
|
}
|
|
@@ -322,24 +408,21 @@ class Analyzer {
|
|
|
322
408
|
case 'null':
|
|
323
409
|
// The empty literal `{}` is statically at most one item, so it satisfies
|
|
324
410
|
// singleton operands (`{} + 1`, `{} and true` are spec-legal).
|
|
325
|
-
return
|
|
411
|
+
return singleState([])
|
|
326
412
|
case 'boolean':
|
|
327
|
-
return
|
|
413
|
+
return singleState(['System.Boolean'])
|
|
328
414
|
case 'string':
|
|
329
|
-
return
|
|
415
|
+
return singleState(['System.String'])
|
|
330
416
|
case 'number':
|
|
331
|
-
return
|
|
332
|
-
types: [node.isLong ? 'System.Long' : node.isDecimal ? 'System.Decimal' : 'System.Integer'],
|
|
333
|
-
single: true,
|
|
334
|
-
}
|
|
417
|
+
return singleState([node.isLong ? 'System.Long' : node.isDecimal ? 'System.Decimal' : 'System.Integer'])
|
|
335
418
|
case 'date':
|
|
336
|
-
return
|
|
419
|
+
return singleState(['System.Date'])
|
|
337
420
|
case 'dateTime':
|
|
338
|
-
return
|
|
421
|
+
return singleState(['System.DateTime'])
|
|
339
422
|
case 'time':
|
|
340
|
-
return
|
|
423
|
+
return singleState(['System.Time'])
|
|
341
424
|
case 'quantity':
|
|
342
|
-
return
|
|
425
|
+
return singleState(['System.Quantity'])
|
|
343
426
|
case 'external':
|
|
344
427
|
return this.walkExternal(node, scope)
|
|
345
428
|
case 'special':
|
|
@@ -353,6 +436,7 @@ class Analyzer {
|
|
|
353
436
|
case 'indexer': {
|
|
354
437
|
const target = this.walk(node.target, input, scope)
|
|
355
438
|
const index = this.walk(node.index, input, scope)
|
|
439
|
+
this.requireOrdered(target, node.target.span, 'The indexer')
|
|
356
440
|
this.requireKind(index, 'Numeric', node.index.span, 'the indexer expects a single Integer')
|
|
357
441
|
return withSingle(target, true)
|
|
358
442
|
}
|
|
@@ -389,7 +473,7 @@ class Analyzer {
|
|
|
389
473
|
}
|
|
390
474
|
const declared = this.declaredVariables.get(node.name)
|
|
391
475
|
if (declared !== undefined) {
|
|
392
|
-
return this.declaredVariableState(declared)
|
|
476
|
+
return this.declaredVariableState(declared, node.name)
|
|
393
477
|
}
|
|
394
478
|
switch (node.name) {
|
|
395
479
|
case 'context':
|
|
@@ -400,13 +484,13 @@ class Analyzer {
|
|
|
400
484
|
case 'ucum':
|
|
401
485
|
case 'sct':
|
|
402
486
|
case 'loinc':
|
|
403
|
-
return
|
|
487
|
+
return singleState(['System.String'])
|
|
404
488
|
default:
|
|
405
489
|
break
|
|
406
490
|
}
|
|
407
491
|
// FHIR-defined families expand to HL7 urls: %`vs-[name]`, %`ext-[name]`.
|
|
408
492
|
if (node.name.startsWith('vs-') || node.name.startsWith('ext-')) {
|
|
409
|
-
return
|
|
493
|
+
return singleState(['System.String'])
|
|
410
494
|
}
|
|
411
495
|
// A dynamically-named defineVariable() earlier in the chain may have bound
|
|
412
496
|
// this name, so reporting it as undefined could be wrong — stay quiet.
|
|
@@ -420,7 +504,7 @@ class Analyzer {
|
|
|
420
504
|
if (name === 'this') {
|
|
421
505
|
return this.frames.at(-1) ?? this.rootState()
|
|
422
506
|
}
|
|
423
|
-
return name === 'index' ?
|
|
507
|
+
return name === 'index' ? singleState(['System.Integer']) : UNKNOWN
|
|
424
508
|
}
|
|
425
509
|
|
|
426
510
|
/**
|
|
@@ -437,7 +521,7 @@ class Analyzer {
|
|
|
437
521
|
private walkIdentifier(node: AstNode & { kind: 'identifier' }, input: StaticState): StaticState {
|
|
438
522
|
// Navigating from a statically empty input yields empty — nothing to check.
|
|
439
523
|
if (input.types !== undefined && input.types.length === 0) {
|
|
440
|
-
return
|
|
524
|
+
return singleState([])
|
|
441
525
|
}
|
|
442
526
|
const exactTypes = input.exactTypes?.filter(type => rootTypeMatches(this.model, type, node.name))
|
|
443
527
|
if (exactTypes !== undefined && exactTypes.length > 0) {
|
|
@@ -445,6 +529,7 @@ class Analyzer {
|
|
|
445
529
|
return {
|
|
446
530
|
types: types === undefined ? undefined : [...new Set(types)],
|
|
447
531
|
single: input.single,
|
|
532
|
+
ordered: singletonOrder(input.single, input.ordered),
|
|
448
533
|
exactTypes,
|
|
449
534
|
...(input.rawInput === true && { rawInput: true }),
|
|
450
535
|
}
|
|
@@ -455,14 +540,22 @@ class Analyzer {
|
|
|
455
540
|
const asType = this.model?.resolveType(node.name) ?? resolveSystemTypeName(node.name)
|
|
456
541
|
if (asType !== undefined) {
|
|
457
542
|
return {
|
|
458
|
-
|
|
459
|
-
single: true,
|
|
543
|
+
...singleState([asType]),
|
|
460
544
|
...(input.exactTypes !== undefined && {
|
|
461
545
|
exactTypes: input.exactTypes.filter(type => this.model?.isSubtypeOf(type, asType) === true),
|
|
462
546
|
}),
|
|
463
547
|
}
|
|
464
548
|
}
|
|
465
|
-
|
|
549
|
+
if (input.opaqueVariable !== undefined && this.reportUnchecked) {
|
|
550
|
+
this.report(
|
|
551
|
+
'unchecked-navigation',
|
|
552
|
+
`Element '${node.name}' after untyped environment variable %${input.opaqueVariable} was not checked; declare the variable's type`,
|
|
553
|
+
node.span,
|
|
554
|
+
'warning',
|
|
555
|
+
node.name
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
return { ...UNKNOWN, ordered: input.ordered }
|
|
466
559
|
}
|
|
467
560
|
// Root rule: an identifier naming the (super)type of the context is the context.
|
|
468
561
|
{
|
|
@@ -528,9 +621,14 @@ class Analyzer {
|
|
|
528
621
|
node.name
|
|
529
622
|
)
|
|
530
623
|
}
|
|
531
|
-
return UNKNOWN
|
|
624
|
+
return { ...UNKNOWN, ordered: input.ordered }
|
|
625
|
+
}
|
|
626
|
+
const single = singleAnd(input.single, !isCollection)
|
|
627
|
+
const state: StaticState = {
|
|
628
|
+
types: [...new Set(found)],
|
|
629
|
+
single,
|
|
630
|
+
ordered: singletonOrder(single, input.ordered),
|
|
532
631
|
}
|
|
533
|
-
const state: StaticState = { types: [...new Set(found)], single: singleAnd(input.single, !isCollection) }
|
|
534
632
|
if (targets !== undefined && targets.length > 0) {
|
|
535
633
|
state.targets = [...new Set(targets)]
|
|
536
634
|
}
|
|
@@ -641,7 +739,11 @@ class Analyzer {
|
|
|
641
739
|
// ofType(X) filters and as(X) casts: both narrow to the named type,
|
|
642
740
|
// intersected with the known candidates.
|
|
643
741
|
if ((node.name === 'ofType' || node.name === 'as') && typeTarget !== undefined) {
|
|
644
|
-
return {
|
|
742
|
+
return {
|
|
743
|
+
types: this.narrowTypes(input, typeTarget, node.span),
|
|
744
|
+
single: input.single,
|
|
745
|
+
ordered: singletonOrder(input.single, input.ordered),
|
|
746
|
+
}
|
|
645
747
|
}
|
|
646
748
|
const expressionResult = this.expressionFunctionResult(node.name, custom, input, scope)
|
|
647
749
|
if (expressionResult !== undefined) {
|
|
@@ -675,7 +777,7 @@ class Analyzer {
|
|
|
675
777
|
const bodyInput = this.expressionBodyInput(declaration, input)
|
|
676
778
|
if (declaration.criteria === true) {
|
|
677
779
|
this.walkExpressionFunction(name, expression, declaration.variables, bodyInput, scope)
|
|
678
|
-
return
|
|
780
|
+
return singleState(['System.Boolean'])
|
|
679
781
|
}
|
|
680
782
|
if (declaration.signature?.result === undefined) {
|
|
681
783
|
return this.walkExpressionFunction(name, expression, declaration.variables, bodyInput, scope)
|
|
@@ -703,6 +805,7 @@ class Analyzer {
|
|
|
703
805
|
return {
|
|
704
806
|
types: [...new Set(types)],
|
|
705
807
|
single: callInput.single,
|
|
808
|
+
ordered: callInput.ordered,
|
|
706
809
|
...(callInput.targets !== undefined && { targets: callInput.targets }),
|
|
707
810
|
}
|
|
708
811
|
}
|
|
@@ -731,7 +834,7 @@ class Analyzer {
|
|
|
731
834
|
const bare = bareEnvironmentName(declaredName)
|
|
732
835
|
// defineVariable() values have priority over environment overlays at runtime.
|
|
733
836
|
if (!functionScope.vars.has(bare)) {
|
|
734
|
-
functionScope.vars.set(bare, this.declaredVariableState(variable))
|
|
837
|
+
functionScope.vars.set(bare, this.declaredVariableState(variable, bare))
|
|
735
838
|
}
|
|
736
839
|
}
|
|
737
840
|
const diagnosticCount = this.diagnostics.length
|
|
@@ -754,13 +857,15 @@ class Analyzer {
|
|
|
754
857
|
}
|
|
755
858
|
|
|
756
859
|
/** Convert one declared environment value into the same state used for scoped variables. */
|
|
757
|
-
private declaredVariableState(declared: AnalyzerVariableState): StaticState {
|
|
860
|
+
private declaredVariableState(declared: AnalyzerVariableState, name?: string): StaticState {
|
|
758
861
|
const exactTypes = this.runtime && isRuntimeAnalyzerVariable(declared) ? declared.exactTypes : undefined
|
|
759
862
|
return {
|
|
760
863
|
types: declared.types?.map(type => this.canonicalize(type)),
|
|
761
864
|
single: declared.single,
|
|
865
|
+
ordered: singletonOrder(declared.single, declared.ordered),
|
|
762
866
|
...(declared.targets !== undefined && { targets: declared.targets.map(type => this.canonicalize(type)) }),
|
|
763
867
|
...(exactTypes !== undefined && { exactTypes }),
|
|
868
|
+
...(declared.types === undefined && name !== undefined && { opaqueVariable: name }),
|
|
764
869
|
}
|
|
765
870
|
}
|
|
766
871
|
|
|
@@ -814,11 +919,14 @@ class Analyzer {
|
|
|
814
919
|
}
|
|
815
920
|
}
|
|
816
921
|
|
|
817
|
-
/** The signature's input constraints: cardinality, value kind, and declared types. */
|
|
922
|
+
/** The signature's input constraints: cardinality, ordering, value kind, and declared types. */
|
|
818
923
|
private checkCallInput(node: AstNode & { kind: 'call' }, signature: FunctionSignature, input: StaticState): void {
|
|
819
924
|
if (!signature.input) {
|
|
820
925
|
return
|
|
821
926
|
}
|
|
927
|
+
if (signature.input.ordered === true) {
|
|
928
|
+
this.requireOrdered(input, node.span, `${node.name}()`)
|
|
929
|
+
}
|
|
822
930
|
// A function written for one type (a DTO's `@column`), called on a focus
|
|
823
931
|
// that can never be that type. `unsatisfiedInput` holds the same rule the
|
|
824
932
|
// engine applies, so the two halves agree on what counts as a mistake.
|
|
@@ -840,7 +948,7 @@ class Analyzer {
|
|
|
840
948
|
}
|
|
841
949
|
if (signature.input.kind) {
|
|
842
950
|
this.requireKind(
|
|
843
|
-
|
|
951
|
+
singleState(input.types),
|
|
844
952
|
signature.input.kind,
|
|
845
953
|
node.span,
|
|
846
954
|
`${node.name}() expects a ${signature.input.kind} input`
|
|
@@ -970,14 +1078,14 @@ class Analyzer {
|
|
|
970
1078
|
return undefined
|
|
971
1079
|
}
|
|
972
1080
|
const types = declared.result?.types?.map(type => this.canonicalize(type))
|
|
973
|
-
const single = declared.result?.single
|
|
974
1081
|
return {
|
|
975
1082
|
...(declared.input !== undefined && { input: declared.input }),
|
|
976
1083
|
...(declared.args !== undefined && { args: declared.args }),
|
|
977
1084
|
result: {
|
|
978
1085
|
kind: 'fixed',
|
|
979
1086
|
...(types !== undefined && { types }),
|
|
980
|
-
...(single !== undefined && { single }),
|
|
1087
|
+
...(declared.result?.single !== undefined && { single: declared.result.single }),
|
|
1088
|
+
...(declared.result?.ordered !== undefined && { ordered: declared.result.ordered }),
|
|
981
1089
|
},
|
|
982
1090
|
}
|
|
983
1091
|
}
|
|
@@ -1124,6 +1232,17 @@ class Analyzer {
|
|
|
1124
1232
|
}
|
|
1125
1233
|
}
|
|
1126
1234
|
|
|
1235
|
+
/** Report an order-dependent operation only when the input is provably unordered. */
|
|
1236
|
+
private requireOrdered(state: StaticState, span: SourceSpan, subject: string): void {
|
|
1237
|
+
if (state.ordered === false && state.single !== true) {
|
|
1238
|
+
this.report(
|
|
1239
|
+
'order-dependent',
|
|
1240
|
+
`${subject} depends on collection order, but the input collection has no defined order`,
|
|
1241
|
+
span
|
|
1242
|
+
)
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1127
1246
|
private walkTypeOp(node: AstNode & { kind: 'typeOp' }, input: StaticState, scope: VariableScope): StaticState {
|
|
1128
1247
|
const operand = this.walk(node.operand, input, scope)
|
|
1129
1248
|
this.requireSingle(operand, node.operand.span, `'${node.operator}' expects a single item operand`)
|
|
@@ -1291,29 +1410,42 @@ function mergedDeclaration(candidates: readonly ResolvedDeclaration[]): Resolved
|
|
|
1291
1410
|
}
|
|
1292
1411
|
}
|
|
1293
1412
|
|
|
1294
|
-
/**
|
|
1413
|
+
/**
|
|
1414
|
+
* Every type any declaration accepts, or no type constraint when one of them
|
|
1415
|
+
* accepts anything. An ordered input stays required when every candidate
|
|
1416
|
+
* requires it — whichever declaration the call resolves to would reject an
|
|
1417
|
+
* unordered focus, so keeping the requirement cannot flag valid code.
|
|
1418
|
+
*/
|
|
1295
1419
|
function mergedInput(candidates: readonly ResolvedDeclaration[]): InputSpec | undefined {
|
|
1420
|
+
const ordered = candidates.every(candidate => candidate.signature?.input?.ordered === true)
|
|
1296
1421
|
const declared = candidates.map(candidate => candidate.signature?.input?.types)
|
|
1297
|
-
|
|
1422
|
+
const types = declared.some(types => types === undefined)
|
|
1423
|
+
? undefined
|
|
1424
|
+
: [...new Set(declared.flatMap(types => types ?? []))]
|
|
1425
|
+
if (types === undefined && !ordered) {
|
|
1298
1426
|
return undefined
|
|
1299
1427
|
}
|
|
1300
|
-
return {
|
|
1428
|
+
return { ...(types !== undefined && { types }), ...(ordered && { ordered: true }) }
|
|
1301
1429
|
}
|
|
1302
1430
|
|
|
1303
1431
|
/** The union of the declarations' results, unknown as soon as one of them is. */
|
|
1304
|
-
function mergedResult(
|
|
1432
|
+
function mergedResult(
|
|
1433
|
+
candidates: readonly ResolvedDeclaration[]
|
|
1434
|
+
): { types?: string[]; single?: boolean; ordered?: boolean } | undefined {
|
|
1305
1435
|
const union = unionStates(
|
|
1306
1436
|
candidates.map(candidate => ({
|
|
1307
1437
|
types: candidate.signature?.result?.types === undefined ? undefined : [...candidate.signature.result.types],
|
|
1308
1438
|
single: candidate.signature?.result?.single,
|
|
1439
|
+
ordered: singletonOrder(candidate.signature?.result?.single, candidate.signature?.result?.ordered),
|
|
1309
1440
|
}))
|
|
1310
1441
|
)
|
|
1311
|
-
if (union.types === undefined && union.single === undefined) {
|
|
1442
|
+
if (union.types === undefined && union.single === undefined && union.ordered === undefined) {
|
|
1312
1443
|
return undefined
|
|
1313
1444
|
}
|
|
1314
1445
|
return {
|
|
1315
1446
|
...(union.types !== undefined && { types: union.types }),
|
|
1316
1447
|
...(union.single !== undefined && { single: union.single }),
|
|
1448
|
+
...(union.ordered !== undefined && { ordered: union.ordered }),
|
|
1317
1449
|
}
|
|
1318
1450
|
}
|
|
1319
1451
|
|
|
@@ -7,11 +7,21 @@ import { OBJECT_TYPE, toCollection, type TypedValue, typeLocalName } from '../va
|
|
|
7
7
|
export interface AnalyzerVariable {
|
|
8
8
|
types?: string[]
|
|
9
9
|
single?: boolean
|
|
10
|
+
/** True: ordered. False: unordered. Omit when ordering is unknown. */
|
|
11
|
+
ordered?: boolean
|
|
10
12
|
targets?: string[]
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
/** Projection variables supplied by the runtime for every row. */
|
|
16
|
+
export const PROJECT_ROW_VARIABLES: Readonly<Record<'rowIndex' | 'rowTotal', AnalyzerVariable>> = {
|
|
17
|
+
rowIndex: { types: ['System.Integer'], single: true },
|
|
18
|
+
rowTotal: { types: ['System.Integer'], single: true },
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
/** Internal variable state with the exact focus types used by runtime host-function dispatch. */
|
|
14
22
|
export interface RuntimeAnalyzerVariable extends AnalyzerVariable {
|
|
23
|
+
/** A runtime collection is a real array, so its order is always defined. */
|
|
24
|
+
ordered: true
|
|
15
25
|
exactTypes: string[]
|
|
16
26
|
}
|
|
17
27
|
|
|
@@ -41,6 +51,7 @@ export function runtimeAnalyzerVariable(
|
|
|
41
51
|
const inferred = analyzerVariableFromCollection(collection, model)
|
|
42
52
|
return {
|
|
43
53
|
...(declaration === undefined ? inferred : analyzerVariable(declaration)),
|
|
54
|
+
ordered: true,
|
|
44
55
|
exactTypes: collection.map(item => item.type),
|
|
45
56
|
}
|
|
46
57
|
}
|
|
@@ -54,6 +65,7 @@ function analyzerVariableFromCollection(
|
|
|
54
65
|
return {
|
|
55
66
|
...(types !== undefined && types.length > 0 && { types }),
|
|
56
67
|
single: collection.length <= 1,
|
|
68
|
+
ordered: true,
|
|
57
69
|
}
|
|
58
70
|
}
|
|
59
71
|
|