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
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
type ColumnTypeClaim,
|
|
10
10
|
criteriaSignature,
|
|
11
11
|
} from '../api/column-signature.ts'
|
|
12
|
+
import { bareEnvironmentName } from '../engine/context.ts'
|
|
13
|
+
import { type AnalyzerVariable, PROJECT_ROW_VARIABLES } from './declarations.ts'
|
|
14
|
+
import type { SourceVariablePlan } from './source-options.ts'
|
|
12
15
|
|
|
13
16
|
/** The supported expression argument shapes: one string, columns, constraints, or DTO variables. */
|
|
14
17
|
export type CallSiteShape = 'expression' | 'columns' | 'constraints' | 'dto-vars'
|
|
@@ -38,6 +41,12 @@ export interface CallSitePolicy {
|
|
|
38
41
|
* the expression is analyzed without an input type.
|
|
39
42
|
*/
|
|
40
43
|
rootFromClass?: true
|
|
44
|
+
/** The EvaluateOptions argument whose inline env/vars declarations are visible to the expression. */
|
|
45
|
+
optionsArg?: number
|
|
46
|
+
/** Additional expressions held by the options argument. */
|
|
47
|
+
optionsExpressions?: 'vars'
|
|
48
|
+
/** The call supplies projection-only `%rowIndex` and `%rowTotal`. */
|
|
49
|
+
rowVariables?: true
|
|
41
50
|
/**
|
|
42
51
|
* A DTO member site. Its `%variables` are never judged — they come from the
|
|
43
52
|
* DTO's own `vars`/`env`, from a base class, or from the projecting call, none
|
|
@@ -58,15 +67,28 @@ export const CALL_SITES: ReadonlyMap<string, CallSitePolicy> = new Map([
|
|
|
58
67
|
// evaluated somewhere else entirely — checkable.
|
|
59
68
|
['fhirpath', { argIndex: 0, shape: 'expression', receiver: 'any', rootArg: 1 }],
|
|
60
69
|
['compile', { argIndex: 0, shape: 'expression', receiver: 'any', rootArg: 1 }],
|
|
61
|
-
['evaluate', { argIndex: 0, shape: 'expression', receiver: 'any' }],
|
|
62
|
-
['evaluateTyped', { argIndex: 0, shape: 'expression', receiver: 'any' }],
|
|
63
|
-
['first', { argIndex: 0, shape: 'expression', receiver: 'engine' }],
|
|
70
|
+
['evaluate', { argIndex: 0, shape: 'expression', receiver: 'any', optionsArg: 2, optionsExpressions: 'vars' }],
|
|
71
|
+
['evaluateTyped', { argIndex: 0, shape: 'expression', receiver: 'any', optionsArg: 2, optionsExpressions: 'vars' }],
|
|
72
|
+
['first', { argIndex: 0, shape: 'expression', receiver: 'engine', optionsArg: 2, optionsExpressions: 'vars' }],
|
|
64
73
|
['analyzeExpression', { argIndex: 0, shape: 'expression', receiver: 'any' }],
|
|
65
74
|
// Subject-first FhirPathEngine helpers: the expression(s) come second.
|
|
66
|
-
['test', { argIndex: 1, shape: 'expression', receiver: 'engine' }],
|
|
67
|
-
['filter', { argIndex: 1, shape: 'expression', receiver: 'engine' }],
|
|
68
|
-
[
|
|
69
|
-
|
|
75
|
+
['test', { argIndex: 1, shape: 'expression', receiver: 'engine', optionsArg: 2, optionsExpressions: 'vars' }],
|
|
76
|
+
['filter', { argIndex: 1, shape: 'expression', receiver: 'engine', optionsArg: 2, optionsExpressions: 'vars' }],
|
|
77
|
+
[
|
|
78
|
+
'project',
|
|
79
|
+
{
|
|
80
|
+
argIndex: 1,
|
|
81
|
+
shape: 'columns',
|
|
82
|
+
receiver: 'engine',
|
|
83
|
+
optionsArg: 2,
|
|
84
|
+
optionsExpressions: 'vars',
|
|
85
|
+
rowVariables: true,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
[
|
|
89
|
+
'checkConstraints',
|
|
90
|
+
{ argIndex: 1, shape: 'constraints', receiver: 'any', optionsArg: 2, optionsExpressions: 'vars' },
|
|
91
|
+
],
|
|
70
92
|
// DTO declarations: the column/criteria expressions of a `@column` field, and
|
|
71
93
|
// the `vars` a DTO binds per row.
|
|
72
94
|
[
|
|
@@ -113,6 +135,12 @@ export interface SourceBindings {
|
|
|
113
135
|
rebound: ReadonlySet<string>
|
|
114
136
|
}
|
|
115
137
|
|
|
138
|
+
/** Semantic receiver facts a compiler-backed walker can prove. */
|
|
139
|
+
export interface ReceiverEvidence {
|
|
140
|
+
/** The receiver resolves to this package's FhirPathEngine declaration. */
|
|
141
|
+
engine?: true
|
|
142
|
+
}
|
|
143
|
+
|
|
116
144
|
/**
|
|
117
145
|
* Whether a call site should be checked, given its policy and the file's
|
|
118
146
|
* bindings. `receiverRoot` is the leftmost identifier of a member-expression
|
|
@@ -124,14 +152,18 @@ export function isCheckedCall(
|
|
|
124
152
|
policy: CallSitePolicy,
|
|
125
153
|
calleeName: string,
|
|
126
154
|
receiverRoot: string | undefined,
|
|
127
|
-
bindings: SourceBindings
|
|
155
|
+
bindings: SourceBindings,
|
|
156
|
+
evidence: ReceiverEvidence = {}
|
|
128
157
|
): boolean {
|
|
129
|
-
if (policy.receiver === 'engine') {
|
|
130
|
-
return receiverRoot !== undefined && bindings.trusted.has(receiverRoot) && !bindings.rebound.has(receiverRoot)
|
|
131
|
-
}
|
|
132
158
|
if (policy.receiver === 'import') {
|
|
133
159
|
return receiverRoot === undefined && bindings.trusted.has(calleeName) && !bindings.rebound.has(calleeName)
|
|
134
160
|
}
|
|
161
|
+
if (evidence.engine === true) {
|
|
162
|
+
return true
|
|
163
|
+
}
|
|
164
|
+
if (policy.receiver === 'engine') {
|
|
165
|
+
return receiverRoot !== undefined && bindings.trusted.has(receiverRoot) && !bindings.rebound.has(receiverRoot)
|
|
166
|
+
}
|
|
135
167
|
return !bindings.foreign.has(receiverRoot ?? calleeName)
|
|
136
168
|
}
|
|
137
169
|
|
|
@@ -193,8 +225,25 @@ export interface SiteContext {
|
|
|
193
225
|
inputType?: string
|
|
194
226
|
/** A DTO member site, whose findings are weighed differently (see `CallSitePolicy.dto`). */
|
|
195
227
|
dto?: true
|
|
228
|
+
/** Inline per-call environment and row-variable declarations visible to this site. */
|
|
229
|
+
variables?: Readonly<Record<string, SiteVariable>>
|
|
230
|
+
/** Ordered per-call vars, kept separate so loaded engine defaults can be merged without losing runtime order. */
|
|
231
|
+
variablePlan?: SiteVariablePlan
|
|
232
|
+
/**
|
|
233
|
+
* The call binds variables the source cannot name: a computed key, a spread,
|
|
234
|
+
* or a non-literal env/vars object. An unresolved `%variable` at such a site
|
|
235
|
+
* may exist at runtime, so it is a coverage gap rather than an error — see
|
|
236
|
+
* `analyzeSite`.
|
|
237
|
+
*/
|
|
238
|
+
openVariables?: true
|
|
196
239
|
}
|
|
197
240
|
|
|
241
|
+
/** Analyzer facts a source literal can declare; ordering requires runtime knowledge. */
|
|
242
|
+
export type SiteVariable = Pick<AnalyzerVariable, 'types' | 'single' | 'targets'>
|
|
243
|
+
|
|
244
|
+
/** The vars state at one expression: declarations apply as each value runs, then to the completed scope. */
|
|
245
|
+
export type SiteVariablePlan = SourceVariablePlan
|
|
246
|
+
|
|
198
247
|
/** One class of a file, as a walker reads its heritage clause. */
|
|
199
248
|
export interface ClassHeritage {
|
|
200
249
|
/** The class's own name, when it has one. */
|
|
@@ -276,7 +325,8 @@ export function siteContext<N>(
|
|
|
276
325
|
policy: CallSitePolicy,
|
|
277
326
|
argumentAt: (index: number) => N | undefined,
|
|
278
327
|
classRoot: string | undefined,
|
|
279
|
-
ast: ExpressionAst<N
|
|
328
|
+
ast: ExpressionAst<N>,
|
|
329
|
+
variables: Readonly<Record<string, SiteVariable>> | undefined
|
|
280
330
|
): SiteContext {
|
|
281
331
|
const rootArgument = policy.rootArg === undefined ? undefined : argumentAt(policy.rootArg)
|
|
282
332
|
const inputType =
|
|
@@ -288,9 +338,205 @@ export function siteContext<N>(
|
|
|
288
338
|
return {
|
|
289
339
|
...(policy.dto === true && { dto: true as const }),
|
|
290
340
|
...(inputType !== undefined && { inputType }),
|
|
341
|
+
...(variables !== undefined && Object.keys(variables).length > 0 && { variables }),
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export interface OptionScopes<N> {
|
|
346
|
+
env: Record<string, SiteVariable>
|
|
347
|
+
vars: Record<string, SiteVariable>
|
|
348
|
+
/** Final declarations only, without value-only names. */
|
|
349
|
+
varDeclarations: Record<string, SiteVariable>
|
|
350
|
+
/** Whether this closed options object definitely omits varTypes. */
|
|
351
|
+
inheritsVarDeclarations: boolean
|
|
352
|
+
/**
|
|
353
|
+
* The options bind variables whose names the source cannot enumerate before
|
|
354
|
+
* any var expression runs: an open `env`/`envTypes` object, or an unresolved
|
|
355
|
+
* options write that may supply either one.
|
|
356
|
+
*/
|
|
357
|
+
openBeforeVars: boolean
|
|
358
|
+
/** Whether the completed env/vars/type declarations may bind additional names. */
|
|
359
|
+
openAfterVars: boolean
|
|
360
|
+
/** Final ordered `vars` entries, or one coverage gap when their order is dynamic. */
|
|
361
|
+
expressions: (ExpressionCandidate<N> & { name?: string })[]
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Parse one literal EvaluateOptions object into environment and ordered var scopes. */
|
|
365
|
+
export function optionScopes<N>(options: N, ast: ExpressionAst<N>): OptionScopes<N> | undefined {
|
|
366
|
+
const properties = ast.properties(options)
|
|
367
|
+
if (properties === undefined) {
|
|
368
|
+
return undefined
|
|
369
|
+
}
|
|
370
|
+
const lastUnknown = properties.findLastIndex(property => property.name === undefined)
|
|
371
|
+
const envValues = finalKnownProperty(properties, 'env', lastUnknown)
|
|
372
|
+
const envDeclarations = finalKnownProperty(properties, 'envTypes', lastUnknown)
|
|
373
|
+
const varValues = finalKnownProperty(properties, 'vars', lastUnknown)
|
|
374
|
+
const varDeclarations = finalKnownProperty(properties, 'varTypes', lastUnknown)
|
|
375
|
+
const varsProperties = varValues === undefined ? [] : ast.properties(varValues)
|
|
376
|
+
const envScope = variablesFromPair(envValues, envDeclarations, ast)
|
|
377
|
+
const varScope = variablesFromPair(varValues, varDeclarations, ast)
|
|
378
|
+
const unknownOptions = lastUnknown >= 0
|
|
379
|
+
// env/envTypes exist while var bodies run. vars/varTypes only finish binding
|
|
380
|
+
// after those bodies have run, so an unknown varTypes name must not hide a
|
|
381
|
+
// real error inside a var expression.
|
|
382
|
+
const openBeforeVars =
|
|
383
|
+
envScope.valuesOpen ||
|
|
384
|
+
envScope.declarationsOpen ||
|
|
385
|
+
(unknownOptions && (envValues === undefined || envDeclarations === undefined))
|
|
386
|
+
const dynamicVars = varsProperties === undefined || varsProperties.some(property => property.name === undefined)
|
|
387
|
+
return {
|
|
388
|
+
env: envScope.variables,
|
|
389
|
+
vars: varScope.variables,
|
|
390
|
+
varDeclarations: varScope.declarations,
|
|
391
|
+
inheritsVarDeclarations: !unknownOptions && varDeclarations === undefined,
|
|
392
|
+
openBeforeVars,
|
|
393
|
+
openAfterVars:
|
|
394
|
+
openBeforeVars ||
|
|
395
|
+
dynamicVars ||
|
|
396
|
+
varScope.declarationsOpen ||
|
|
397
|
+
(unknownOptions && (varValues === undefined || varDeclarations === undefined)),
|
|
398
|
+
expressions:
|
|
399
|
+
varValues === undefined
|
|
400
|
+
? []
|
|
401
|
+
: dynamicVars
|
|
402
|
+
? [{ node: varValues, uncheckable: 'dynamic-vars' as const }]
|
|
403
|
+
: finalVarExpressions(varsProperties, ast),
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Final values in Object.entries order when every vars key is statically known. */
|
|
408
|
+
function finalVarExpressions<N>(
|
|
409
|
+
properties: readonly ExpressionProperty<N>[],
|
|
410
|
+
ast: ExpressionAst<N>
|
|
411
|
+
): (ExpressionCandidate<N> & { name: string })[] {
|
|
412
|
+
return finalNormalizedEntries(properties).flatMap(({ name, value: node }) => {
|
|
413
|
+
const entry = ast.string(node)
|
|
414
|
+
return [{ node, name, ...(entry !== undefined && { expression: entry.expression }) }]
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Runtime entries after Object.entries(), bare-name normalization, and Object.entries() again. */
|
|
419
|
+
function finalNormalizedEntries<N>(properties: readonly ExpressionProperty<N>[]): { name: string; value: N }[] {
|
|
420
|
+
const raw: Record<string, N> = Object.create(null)
|
|
421
|
+
for (const property of properties) {
|
|
422
|
+
if (property.name !== undefined) {
|
|
423
|
+
raw[property.name] = property.value
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const normalized: Record<string, N> = Object.create(null)
|
|
427
|
+
for (const [rawName, value] of Object.entries(raw)) {
|
|
428
|
+
normalized[bareEnvironmentName(rawName)] = value
|
|
429
|
+
}
|
|
430
|
+
return Object.entries(normalized).map(([name, value]) => ({ name, value }))
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** Known names and provably final values of one normalized host map. */
|
|
434
|
+
function normalizedObjectWrites<N>(
|
|
435
|
+
node: N | undefined,
|
|
436
|
+
ast: ExpressionAst<N>
|
|
437
|
+
): {
|
|
438
|
+
names: string[]
|
|
439
|
+
final: { name: string; value: N }[]
|
|
440
|
+
open: boolean
|
|
441
|
+
} {
|
|
442
|
+
if (node === undefined) {
|
|
443
|
+
return { names: [], final: [], open: false }
|
|
444
|
+
}
|
|
445
|
+
const properties = ast.properties(node)
|
|
446
|
+
if (properties === undefined) {
|
|
447
|
+
return { names: [], final: [], open: true }
|
|
448
|
+
}
|
|
449
|
+
const names = [
|
|
450
|
+
...new Set(
|
|
451
|
+
properties.flatMap(property => (property.name === undefined ? [] : [bareEnvironmentName(property.name)]))
|
|
452
|
+
),
|
|
453
|
+
]
|
|
454
|
+
const open = properties.some(property => property.name === undefined)
|
|
455
|
+
// An unknown raw key can establish either `%x` or `x` first. Overwriting a
|
|
456
|
+
// property does not move that insertion position, so no normalized value is
|
|
457
|
+
// provably final once the map is open.
|
|
458
|
+
const final = open ? [] : finalNormalizedEntries(properties)
|
|
459
|
+
return { names, final, open }
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function variablesFromPair<N>(
|
|
463
|
+
values: N | undefined,
|
|
464
|
+
declared: N | undefined,
|
|
465
|
+
ast: ExpressionAst<N>
|
|
466
|
+
): {
|
|
467
|
+
variables: Record<string, SiteVariable>
|
|
468
|
+
declarations: Record<string, SiteVariable>
|
|
469
|
+
valuesOpen: boolean
|
|
470
|
+
declarationsOpen: boolean
|
|
471
|
+
} {
|
|
472
|
+
const valueWrites = normalizedObjectWrites(values, ast)
|
|
473
|
+
const declarationWrites = normalizedObjectWrites(declared, ast)
|
|
474
|
+
const variables = Object.fromEntries(valueWrites.names.map(name => [name, {}]))
|
|
475
|
+
const declarations: Record<string, SiteVariable> = {}
|
|
476
|
+
for (const { name, value } of declarationWrites.final) {
|
|
477
|
+
const declaration = typeDeclaration(value, ast)
|
|
478
|
+
if (declaration !== undefined) {
|
|
479
|
+
declarations[name] = declaration
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
variables: { ...variables, ...declarations },
|
|
484
|
+
declarations,
|
|
485
|
+
valuesOpen: valueWrites.open,
|
|
486
|
+
declarationsOpen: declarationWrites.open,
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Last named property after an unknown write that could replace its value. */
|
|
491
|
+
function finalKnownProperty<N>(
|
|
492
|
+
properties: readonly ExpressionProperty<N>[] | undefined,
|
|
493
|
+
name: string,
|
|
494
|
+
after: number
|
|
495
|
+
): N | undefined {
|
|
496
|
+
let value: N | undefined
|
|
497
|
+
for (const property of properties?.slice(after + 1) ?? []) {
|
|
498
|
+
if (property.name === name) {
|
|
499
|
+
value = property.value
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return value
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function typeDeclaration<N>(node: N, ast: ExpressionAst<N>): SiteVariable | undefined {
|
|
506
|
+
const properties = ast.properties(node)
|
|
507
|
+
if (properties === undefined) {
|
|
508
|
+
return undefined
|
|
509
|
+
}
|
|
510
|
+
const lastUnknown = properties.findLastIndex(property => property.name === undefined)
|
|
511
|
+
const typeNode = finalKnownProperty(properties, 'type', lastUnknown)
|
|
512
|
+
const types = typeNode === undefined ? undefined : literalStrings(typeNode, ast)
|
|
513
|
+
if (types === undefined || types.length === 0) {
|
|
514
|
+
return undefined
|
|
515
|
+
}
|
|
516
|
+
const collectionNode = finalKnownProperty(properties, 'collection', lastUnknown)
|
|
517
|
+
const collection = collectionNode === undefined ? (lastUnknown < 0 ? false : undefined) : ast.boolean(collectionNode)
|
|
518
|
+
const targetsNode = finalKnownProperty(properties, 'targets', lastUnknown)
|
|
519
|
+
const targets = targetsNode === undefined ? undefined : literalStrings(targetsNode, ast)
|
|
520
|
+
return {
|
|
521
|
+
types,
|
|
522
|
+
...(collection !== undefined && { single: !collection }),
|
|
523
|
+
...(targets !== undefined && targets.length > 0 && { targets }),
|
|
291
524
|
}
|
|
292
525
|
}
|
|
293
526
|
|
|
527
|
+
function literalStrings<N>(node: N, ast: ExpressionAst<N>): string[] | undefined {
|
|
528
|
+
const one = ast.string(node)?.expression
|
|
529
|
+
if (one !== undefined) {
|
|
530
|
+
return [one]
|
|
531
|
+
}
|
|
532
|
+
const elements = ast.elements(node)
|
|
533
|
+
if (elements === undefined) {
|
|
534
|
+
return undefined
|
|
535
|
+
}
|
|
536
|
+
const strings = elements.map(element => ast.string(element)?.expression)
|
|
537
|
+
return strings.every(value => value !== undefined) ? strings : undefined
|
|
538
|
+
}
|
|
539
|
+
|
|
294
540
|
/**
|
|
295
541
|
* The AST accessors a walker provides so shape extraction can be written once.
|
|
296
542
|
* Each returns undefined when the node is not of the asked-for kind, which the
|
|
@@ -303,57 +549,180 @@ export interface ExpressionAst<N> {
|
|
|
303
549
|
/** The node's value as a `true`/`false` literal, or undefined when it is not one. */
|
|
304
550
|
boolean(node: N): boolean | undefined
|
|
305
551
|
/**
|
|
306
|
-
* The
|
|
307
|
-
*
|
|
308
|
-
* statically-known key —
|
|
552
|
+
* The properties of an object literal in source order, including shorthand
|
|
553
|
+
* properties whose value is the identifier itself, or undefined when the node
|
|
554
|
+
* is not an object literal. `name` is the property's statically-known key —
|
|
555
|
+
* a plain or computed string literal, or an identifier — and undefined for
|
|
556
|
+
* other computed keys. A spread carries `spread: true`, no name, and the
|
|
557
|
+
* spread expression as its value.
|
|
309
558
|
*/
|
|
310
|
-
properties(node: N):
|
|
559
|
+
properties(node: N): ExpressionProperty<N>[] | undefined
|
|
311
560
|
/** The elements of an array literal, or undefined when the node is not one. */
|
|
312
561
|
elements(node: N): N[] | undefined
|
|
313
562
|
}
|
|
314
563
|
|
|
564
|
+
/** One object-literal write as exposed by either source walker. */
|
|
565
|
+
export interface ExpressionProperty<N> {
|
|
566
|
+
name: string | undefined
|
|
567
|
+
value: N
|
|
568
|
+
spread?: true
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** One expression-shaped node, whether or not its value is a static string. */
|
|
572
|
+
export interface ExpressionCandidate<N> {
|
|
573
|
+
node: N
|
|
574
|
+
expression?: string
|
|
575
|
+
/** The node contains expressions, but their final names/order cannot be proven. */
|
|
576
|
+
uncheckable?: 'dynamic-vars'
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** One call expression paired with the source facts visible at that exact point. */
|
|
580
|
+
export interface ContextualExpressionCandidate<N> extends ExpressionCandidate<N> {
|
|
581
|
+
context: SiteContext
|
|
582
|
+
source: 'argument' | 'option-var'
|
|
583
|
+
}
|
|
584
|
+
|
|
315
585
|
/**
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
586
|
+
* Expand one supported call into every expression-shaped node and its context.
|
|
587
|
+
* EvaluateOptions vars bind in declaration order after env. Project vars also
|
|
588
|
+
* see the row variables, and project columns see the completed var scope.
|
|
589
|
+
* A construct that binds names the source cannot list marks the relevant scope
|
|
590
|
+
* open. Dynamic `vars` keys also make the final expression values and
|
|
591
|
+
* Object.entries order unknowable: a later spread can overwrite an earlier
|
|
592
|
+
* value while retaining its original key position. Such var bodies become one
|
|
593
|
+
* explicit coverage gap instead of producing false diagnostics.
|
|
320
594
|
*/
|
|
321
|
-
export function
|
|
595
|
+
export function callExpressionCandidates<N>(
|
|
596
|
+
policy: CallSitePolicy,
|
|
597
|
+
argumentAt: (index: number) => N | undefined,
|
|
598
|
+
classRoot: string | undefined,
|
|
599
|
+
ast: ExpressionAst<N>
|
|
600
|
+
): ContextualExpressionCandidate<N>[] {
|
|
601
|
+
const argument = argumentAt(policy.argIndex)
|
|
602
|
+
if (argument === undefined) {
|
|
603
|
+
return []
|
|
604
|
+
}
|
|
605
|
+
const options = policy.optionsArg === undefined ? undefined : argumentAt(policy.optionsArg)
|
|
606
|
+
const scopes = options === undefined ? undefined : optionScopes(options, ast)
|
|
607
|
+
const openBeforeVars = options !== undefined && (scopes === undefined || scopes.openBeforeVars)
|
|
608
|
+
const openAfterVars = options !== undefined && (scopes === undefined || scopes.openAfterVars)
|
|
609
|
+
const base = siteContext(policy, argumentAt, classRoot, ast, {
|
|
610
|
+
...scopes?.env,
|
|
611
|
+
...(policy.rowVariables === true && PROJECT_ROW_VARIABLES),
|
|
612
|
+
})
|
|
613
|
+
const values = scopes?.expressions.flatMap(entry => (entry.name === undefined ? [] : [entry.name])) ?? []
|
|
614
|
+
const plan =
|
|
615
|
+
options === undefined
|
|
616
|
+
? undefined
|
|
617
|
+
: {
|
|
618
|
+
values,
|
|
619
|
+
declarations: scopes?.varDeclarations ?? {},
|
|
620
|
+
inheritsDeclarations: scopes?.inheritsVarDeclarations ?? false,
|
|
621
|
+
}
|
|
622
|
+
const context: SiteContext = {
|
|
623
|
+
...base,
|
|
624
|
+
...(plan !== undefined && { variablePlan: plan }),
|
|
625
|
+
...(openAfterVars && { openVariables: true }),
|
|
626
|
+
}
|
|
627
|
+
const candidates: ContextualExpressionCandidate<N>[] = expressionCandidates(argument, policy.shape, ast).map(
|
|
628
|
+
candidate => ({
|
|
629
|
+
...candidate,
|
|
630
|
+
context,
|
|
631
|
+
source: 'argument' as const,
|
|
632
|
+
})
|
|
633
|
+
)
|
|
634
|
+
if (policy.optionsExpressions !== 'vars' || policy.optionsArg === undefined) {
|
|
635
|
+
return candidates
|
|
636
|
+
}
|
|
637
|
+
if (scopes === undefined) {
|
|
638
|
+
return candidates
|
|
639
|
+
}
|
|
640
|
+
for (const entry of scopes.expressions) {
|
|
641
|
+
candidates.push({
|
|
642
|
+
node: entry.node,
|
|
643
|
+
...(entry.expression !== undefined && { expression: entry.expression }),
|
|
644
|
+
...(entry.uncheckable !== undefined && { uncheckable: entry.uncheckable }),
|
|
645
|
+
context: {
|
|
646
|
+
...base,
|
|
647
|
+
variablePlan: {
|
|
648
|
+
values,
|
|
649
|
+
declarations: scopes.varDeclarations,
|
|
650
|
+
inheritsDeclarations: scopes.inheritsVarDeclarations,
|
|
651
|
+
...(entry.name !== undefined && { before: entry.name }),
|
|
652
|
+
},
|
|
653
|
+
...(openBeforeVars && { openVariables: true as const }),
|
|
654
|
+
},
|
|
655
|
+
source: 'option-var',
|
|
656
|
+
})
|
|
657
|
+
}
|
|
658
|
+
return candidates
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** Canonical traversal of every supported expression container. */
|
|
662
|
+
export function expressionCandidates<N>(
|
|
663
|
+
argument: N,
|
|
664
|
+
shape: CallSiteShape,
|
|
665
|
+
ast: ExpressionAst<N>
|
|
666
|
+
): ExpressionCandidate<N>[] {
|
|
322
667
|
if (shape === 'expression') {
|
|
323
668
|
const entry = ast.string(argument)
|
|
324
|
-
return
|
|
669
|
+
return [{ node: argument, ...(entry !== undefined && { expression: entry.expression }) }]
|
|
325
670
|
}
|
|
326
671
|
if (shape === 'columns') {
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
return (ast.properties(argument) ?? []).flatMap(({ value }) => {
|
|
672
|
+
// A non-object may be a DTO class, which holds no source expressions. A
|
|
673
|
+
// spread holds no column literal of its own.
|
|
674
|
+
return (ast.properties(argument) ?? []).flatMap(({ value, spread }) => {
|
|
675
|
+
if (spread === true) {
|
|
676
|
+
return []
|
|
677
|
+
}
|
|
330
678
|
const entry = ast.string(value)
|
|
331
|
-
|
|
679
|
+
if (entry !== undefined) {
|
|
680
|
+
return [{ node: value, expression: entry.expression }]
|
|
681
|
+
}
|
|
682
|
+
const nested = ast.properties(value)
|
|
683
|
+
if (nested === undefined) {
|
|
684
|
+
return [{ node: value }]
|
|
685
|
+
}
|
|
686
|
+
return nested.flatMap(property => {
|
|
687
|
+
if (property.name !== 'path' && property.name !== 'test') {
|
|
688
|
+
return []
|
|
689
|
+
}
|
|
690
|
+
const nestedEntry = ast.string(property.value)
|
|
691
|
+
return [{ node: property.value, ...(nestedEntry !== undefined && { expression: nestedEntry.expression }) }]
|
|
692
|
+
})
|
|
332
693
|
})
|
|
333
694
|
}
|
|
334
695
|
if (shape === 'dto-vars') {
|
|
335
|
-
// defineDto() options: { vars: { name: 'expr' }, callerEnv: [...] }. Only
|
|
336
|
-
// vars hold expressions.
|
|
337
696
|
const vars = (ast.properties(argument) ?? []).filter(({ name }) => name === 'vars')
|
|
338
|
-
return vars.flatMap(({ value }) =>
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
697
|
+
return vars.flatMap(({ value }) => {
|
|
698
|
+
const properties = ast.properties(value)
|
|
699
|
+
return properties === undefined
|
|
700
|
+
? [{ node: value }]
|
|
701
|
+
: properties.flatMap(property => {
|
|
702
|
+
if (property.spread === true) {
|
|
703
|
+
return []
|
|
704
|
+
}
|
|
705
|
+
const entry = ast.string(property.value)
|
|
706
|
+
return [{ node: property.value, ...(entry !== undefined && { expression: entry.expression }) }]
|
|
707
|
+
})
|
|
708
|
+
})
|
|
344
709
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
return []
|
|
710
|
+
const elements = ast.elements(argument)
|
|
711
|
+
if (elements === undefined) {
|
|
712
|
+
return [{ node: argument }]
|
|
713
|
+
}
|
|
714
|
+
return elements.flatMap(element => {
|
|
715
|
+
const properties = ast.properties(element)
|
|
716
|
+
if (properties === undefined) {
|
|
717
|
+
return [{ node: element }]
|
|
354
718
|
}
|
|
355
|
-
|
|
356
|
-
|
|
719
|
+
return properties.flatMap(property => {
|
|
720
|
+
if (property.name !== 'expression') {
|
|
721
|
+
return []
|
|
722
|
+
}
|
|
723
|
+
const entry = ast.string(property.value)
|
|
724
|
+
return [{ node: property.value, ...(entry !== undefined && { expression: entry.expression }) }]
|
|
725
|
+
})
|
|
357
726
|
})
|
|
358
727
|
}
|
|
359
728
|
|
|
@@ -417,9 +786,16 @@ function columnClaim<N>(options: N | undefined, ast: ExpressionAst<N>): ColumnTy
|
|
|
417
786
|
if (properties === undefined) {
|
|
418
787
|
return {}
|
|
419
788
|
}
|
|
420
|
-
const
|
|
789
|
+
const lastUnknown = properties.findLastIndex(property => property.name === undefined)
|
|
790
|
+
if (lastUnknown >= 0) {
|
|
791
|
+
// A spread/computed key can introduce `as` or `choices`; later type and
|
|
792
|
+
// collection writes do not remove those conversion modes. Keep only the
|
|
793
|
+
// independently known input signature.
|
|
794
|
+
return {}
|
|
795
|
+
}
|
|
796
|
+
const named = (name: string): N | undefined => finalKnownProperty(properties, name, lastUnknown)
|
|
421
797
|
const collection = named('collection')
|
|
422
|
-
const isCollection = collection === undefined ? false : ast.boolean(collection)
|
|
798
|
+
const isCollection = collection === undefined ? (lastUnknown < 0 ? false : undefined) : ast.boolean(collection)
|
|
423
799
|
if (isCollection === undefined) {
|
|
424
800
|
return {}
|
|
425
801
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BinaryOperator } from '../parser/ast.ts'
|
|
2
2
|
import { commonValueKind } from '../values/type-compat.ts'
|
|
3
|
-
import { type StaticStateLike, unionStates, withSingle } from './signatures.ts'
|
|
3
|
+
import { sequentialOrder, type StaticStateLike, unionStates, withOrder, withSingle } from './signatures.ts'
|
|
4
4
|
|
|
5
5
|
export type OperatorResultRule =
|
|
6
6
|
| { kind: 'fixed'; types: readonly string[]; single: boolean }
|
|
@@ -50,7 +50,7 @@ export function applyOperatorResultRule(
|
|
|
50
50
|
const rule = OPERATOR_RESULT_RULES[operator]
|
|
51
51
|
switch (rule.kind) {
|
|
52
52
|
case 'fixed':
|
|
53
|
-
return { types: [...rule.types], single: rule.single }
|
|
53
|
+
return { types: [...rule.types], single: rule.single, ordered: true }
|
|
54
54
|
case 'arithmetic': {
|
|
55
55
|
const leftKind = commonValueKind(left.types)
|
|
56
56
|
const rightKind = commonValueKind(right.types)
|
|
@@ -58,6 +58,7 @@ export function applyOperatorResultRule(
|
|
|
58
58
|
return {
|
|
59
59
|
types: quantity ? ['System.Quantity'] : operator === '/' ? ['System.Decimal'] : (left.types ?? right.types),
|
|
60
60
|
single: true,
|
|
61
|
+
ordered: true,
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
case 'union':
|
|
@@ -67,7 +68,7 @@ export function applyOperatorResultRule(
|
|
|
67
68
|
if (right.types?.length === 0) {
|
|
68
69
|
return left
|
|
69
70
|
}
|
|
70
|
-
return withSingle(unionStates([left, right]), false)
|
|
71
|
+
return withOrder(withSingle(unionStates([left, right]), false), sequentialOrder(left.ordered, right.ordered))
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -79,10 +80,10 @@ export function applyTypeOperatorResultRule(
|
|
|
79
80
|
const rule = TYPE_OPERATOR_RESULT_RULES[operator]
|
|
80
81
|
switch (rule.kind) {
|
|
81
82
|
case 'fixed':
|
|
82
|
-
return { types: [...rule.types], single: rule.single }
|
|
83
|
+
return { types: [...rule.types], single: rule.single, ordered: true }
|
|
83
84
|
case 'narrow':
|
|
84
85
|
return narrowedTypes === undefined
|
|
85
|
-
? { types: undefined, single: undefined }
|
|
86
|
-
: { types: [...narrowedTypes], single: true }
|
|
86
|
+
? { types: undefined, single: undefined, ordered: undefined }
|
|
87
|
+
: { types: [...narrowedTypes], single: true, ordered: true }
|
|
87
88
|
}
|
|
88
89
|
}
|