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.
Files changed (64) hide show
  1. package/README.md +12 -5
  2. package/dist/analyzer/analyze-dto.d.ts.map +1 -1
  3. package/dist/analyzer/analyze-dto.js +14 -8
  4. package/dist/analyzer/analyze-dto.js.map +1 -1
  5. package/dist/analyzer/analyze.d.ts +23 -12
  6. package/dist/analyzer/analyze.d.ts.map +1 -1
  7. package/dist/analyzer/analyze.js +138 -46
  8. package/dist/analyzer/analyze.js.map +1 -1
  9. package/dist/analyzer/declarations.d.ts +6 -0
  10. package/dist/analyzer/declarations.d.ts.map +1 -1
  11. package/dist/analyzer/declarations.js +7 -0
  12. package/dist/analyzer/declarations.js.map +1 -1
  13. package/dist/analyzer/expression-policy.d.ts +88 -14
  14. package/dist/analyzer/expression-policy.d.ts.map +1 -1
  15. package/dist/analyzer/expression-policy.js +296 -41
  16. package/dist/analyzer/expression-policy.js.map +1 -1
  17. package/dist/analyzer/operator-rules.d.ts.map +1 -1
  18. package/dist/analyzer/operator-rules.js +7 -6
  19. package/dist/analyzer/operator-rules.js.map +1 -1
  20. package/dist/analyzer/signatures.d.ts +49 -0
  21. package/dist/analyzer/signatures.d.ts.map +1 -1
  22. package/dist/analyzer/signatures.js +76 -31
  23. package/dist/analyzer/signatures.js.map +1 -1
  24. package/dist/analyzer/source-options.d.ts +17 -0
  25. package/dist/analyzer/source-options.d.ts.map +1 -0
  26. package/dist/analyzer/source-options.js +3 -0
  27. package/dist/analyzer/source-options.js.map +1 -0
  28. package/dist/api/dto.d.ts +9 -4
  29. package/dist/api/dto.d.ts.map +1 -1
  30. package/dist/api/dto.js +14 -3
  31. package/dist/api/dto.js.map +1 -1
  32. package/dist/api/strict.d.ts.map +1 -1
  33. package/dist/api/strict.js +8 -3
  34. package/dist/api/strict.js.map +1 -1
  35. package/dist/cli/dto-check.d.ts +9 -0
  36. package/dist/cli/dto-check.d.ts.map +1 -1
  37. package/dist/cli/dto-check.js +49 -6
  38. package/dist/cli/dto-check.js.map +1 -1
  39. package/dist/cli/fhirpath-check.js +161 -31
  40. package/dist/cli/fhirpath-check.js.map +1 -1
  41. package/dist/eslint/index.d.ts.map +1 -1
  42. package/dist/eslint/index.js +13 -8
  43. package/dist/eslint/index.js.map +1 -1
  44. package/dist/sites/index.d.ts +31 -2
  45. package/dist/sites/index.d.ts.map +1 -1
  46. package/dist/sites/index.js +351 -30
  47. package/dist/sites/index.js.map +1 -1
  48. package/dist/typed/infer.d.ts +2 -0
  49. package/dist/typed/infer.d.ts.map +1 -1
  50. package/package.json +1 -1
  51. package/src/analyzer/analyze-dto.ts +19 -9
  52. package/src/analyzer/analyze.ts +186 -54
  53. package/src/analyzer/declarations.ts +12 -0
  54. package/src/analyzer/expression-policy.ts +423 -47
  55. package/src/analyzer/operator-rules.ts +7 -6
  56. package/src/analyzer/signatures.ts +100 -36
  57. package/src/analyzer/source-options.ts +19 -0
  58. package/src/api/dto.ts +35 -9
  59. package/src/api/strict.ts +8 -3
  60. package/src/cli/dto-check.ts +56 -6
  61. package/src/cli/fhirpath-check.ts +180 -34
  62. package/src/eslint/index.ts +15 -12
  63. package/src/sites/index.ts +428 -40
  64. 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
- ['project', { argIndex: 1, shape: 'columns', receiver: 'engine' }],
69
- ['checkConstraints', { argIndex: 1, shape: 'constraints', receiver: 'any' }],
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 plain (non-spread, non-shorthand) properties of an object literal, or
307
- * undefined when the node is not an object literal. `name` is the property's
308
- * statically-known key — undefined for computed keys.
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): { name: string | undefined; value: N }[] | undefined
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
- * Extract the expression literal(s) an argument holds, per the call site's shape.
317
- * Column keys are output names, not expressions, so a computed key does not stop
318
- * its value from being checked; `{ path }` / `{ expression }` lookups match
319
- * identifier and string-literal keys only.
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 expressionEntries<N>(argument: N, shape: CallSiteShape, ast: ExpressionAst<N>): ExpressionEntry<N>[] {
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 entry ? [entry] : []
669
+ return [{ node: argument, ...(entry !== undefined && { expression: entry.expression }) }]
325
670
  }
326
671
  if (shape === 'columns') {
327
- // project() columns: { name: 'expr' }, { name: { path: 'expr', ... } }, or
328
- // the boolean-criteria form { name: { test: 'expr' } }.
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
- return entry ? [entry] : [...namedStringEntries(value, 'path', ast), ...namedStringEntries(value, 'test', ast)]
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
- (ast.properties(value) ?? []).flatMap(({ value: expression }) => {
340
- const entry = ast.string(expression)
341
- return entry ? [entry] : []
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
- // checkConstraints() constraints: [{ key, expression: 'expr', ... }].
346
- return (ast.elements(argument) ?? []).flatMap(element => namedStringEntries(element, 'expression', ast))
347
- }
348
-
349
- /** The string-literal values of an object literal's `name` properties. */
350
- function namedStringEntries<N>(object: N, name: string, ast: ExpressionAst<N>): ExpressionEntry<N>[] {
351
- return (ast.properties(object) ?? []).flatMap(({ name: key, value }) => {
352
- if (key !== name) {
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
- const entry = ast.string(value)
356
- return entry ? [entry] : []
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 named = (name: string): N | undefined => properties.find(property => property.name === name)?.value
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
  }