fhirpath-ts 0.2.0 → 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/r4/generated/resources-data.d.ts +6 -2
- package/dist/r4/generated/resources-data.d.ts.map +1 -1
- package/dist/r4/generated/resources-data.js +626 -28627
- package/dist/r4/generated/resources-data.js.map +1 -1
- package/dist/r4/generated/types-data.d.ts +6 -2
- package/dist/r4/generated/types-data.d.ts.map +1 -1
- package/dist/r4/generated/types-data.js +84 -2204
- package/dist/r4/generated/types-data.js.map +1 -1
- package/dist/r4/index.d.ts.map +1 -1
- package/dist/r4/index.js +10 -4
- package/dist/r4/index.js.map +1 -1
- package/dist/r4/model-data.d.ts +17 -0
- package/dist/r4/model-data.d.ts.map +1 -1
- package/dist/r4/model-data.js +118 -1
- package/dist/r4/model-data.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/r4/generated/resources-data.ts +624 -28628
- package/src/r4/generated/types-data.ts +82 -2205
- package/src/r4/index.ts +11 -5
- package/src/r4/model-data.ts +127 -0
- package/src/sites/index.ts +428 -40
- package/src/typed/infer.ts +2 -2
package/src/sites/index.ts
CHANGED
|
@@ -12,14 +12,16 @@ import type TS from 'typescript'
|
|
|
12
12
|
|
|
13
13
|
import {
|
|
14
14
|
CALL_SITES,
|
|
15
|
+
callExpressionCandidates,
|
|
15
16
|
type ClassHeritage,
|
|
16
17
|
columnFunctionDeclaration,
|
|
17
18
|
constructsEngine,
|
|
18
19
|
declaredColumnOverloads,
|
|
19
20
|
DTO_BASE_NAME,
|
|
20
21
|
dtoRootsOf,
|
|
22
|
+
ENGINE_CLASS_NAME,
|
|
21
23
|
type ExpressionAst,
|
|
22
|
-
|
|
24
|
+
type ExpressionProperty,
|
|
23
25
|
type FileColumnFunction,
|
|
24
26
|
isCheckedCall,
|
|
25
27
|
isCheckedTag,
|
|
@@ -27,7 +29,6 @@ import {
|
|
|
27
29
|
type LocalModuleOptions,
|
|
28
30
|
rootOf,
|
|
29
31
|
type SiteContext,
|
|
30
|
-
siteContext,
|
|
31
32
|
type SourceBindings,
|
|
32
33
|
TAG_NAME,
|
|
33
34
|
} from '../analyzer/expression-policy.ts'
|
|
@@ -45,19 +46,52 @@ export interface ExpressionSite {
|
|
|
45
46
|
inputType?: string
|
|
46
47
|
/** A DTO member site, which `analyzeSite` checks with source-only limits. */
|
|
47
48
|
dto?: true
|
|
49
|
+
/** Inline per-call environment and row-variable declarations visible to this expression. */
|
|
50
|
+
variables?: NonNullable<SiteContext['variables']>
|
|
51
|
+
/** Ordered per-call vars, interpreted together with loaded engine defaults by analyzeSite. */
|
|
52
|
+
variablePlan?: NonNullable<SiteContext['variablePlan']>
|
|
53
|
+
/** The call binds variables the source cannot name (a computed key or spread) — see `SiteContext`. */
|
|
54
|
+
openVariables?: true
|
|
48
55
|
/** Callable DTO columns declared in the same file. */
|
|
49
56
|
functions?: Readonly<Record<string, FileColumnFunction>>
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
export interface SkippedExpressionSite {
|
|
60
|
+
/** Stable reason suitable for CLI/editor policy. */
|
|
61
|
+
reason: 'dynamic-expression' | 'unrecognized-receiver'
|
|
62
|
+
message: string
|
|
63
|
+
line: number
|
|
64
|
+
column: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DtoSourceDeclaration {
|
|
68
|
+
name: string
|
|
69
|
+
line: number
|
|
70
|
+
column: number
|
|
71
|
+
/** True when importing the module can enumerate this class, directly or through an exported subclass. */
|
|
72
|
+
loadable: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface SiteScanResult {
|
|
76
|
+
sites: ExpressionSite[]
|
|
77
|
+
skipped: SkippedExpressionSite[]
|
|
78
|
+
dtoDeclarations: DtoSourceDeclaration[]
|
|
79
|
+
}
|
|
80
|
+
|
|
52
81
|
/** Finds every static FHIRPath expression in one source file. */
|
|
53
82
|
export type SiteFinder = (sourceText: string, fileName: string, options?: LocalModuleOptions) => ExpressionSite[]
|
|
54
83
|
|
|
84
|
+
/** Scans expressions plus coverage gaps that a caller may choose to report. */
|
|
85
|
+
export type SiteScanner = (sourceText: string, fileName: string, options?: LocalModuleOptions) => SiteScanResult
|
|
86
|
+
|
|
55
87
|
/**
|
|
56
88
|
* Creates a finder for the calls and tags in `CALL_SITES`. DTO fields include a
|
|
57
89
|
* root type when the class declares one, and each column is exposed as a local
|
|
58
90
|
* function declaration. Dynamic expressions are skipped.
|
|
59
91
|
*/
|
|
60
|
-
export function
|
|
92
|
+
export function createSiteScanner(ts: TypeScriptApi, program?: TS.Program): SiteScanner {
|
|
93
|
+
const checker = program?.getTypeChecker()
|
|
94
|
+
const engineSymbolsByOptions = new Map<string, ReadonlySet<TS.Symbol>>()
|
|
61
95
|
/** How the shared shape extractor reads TypeScript AST nodes. */
|
|
62
96
|
const tsAst: ExpressionAst<TS.Node> = {
|
|
63
97
|
string: node => (ts.isStringLiteralLike(node) ? { node, expression: node.text } : undefined),
|
|
@@ -65,15 +99,33 @@ export function createSiteFinder(ts: TypeScriptApi): SiteFinder {
|
|
|
65
99
|
node.kind === ts.SyntaxKind.TrueKeyword ? true : node.kind === ts.SyntaxKind.FalseKeyword ? false : undefined,
|
|
66
100
|
properties: node =>
|
|
67
101
|
ts.isObjectLiteralExpression(node)
|
|
68
|
-
? node.properties
|
|
69
|
-
|
|
70
|
-
|
|
102
|
+
? node.properties.flatMap<ExpressionProperty<TS.Node>>(property => {
|
|
103
|
+
if (ts.isPropertyAssignment(property)) {
|
|
104
|
+
return [{ name: propertyKeyName(property.name), value: property.initializer }]
|
|
105
|
+
}
|
|
106
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
107
|
+
return [{ name: property.name.text, value: property.name }]
|
|
108
|
+
}
|
|
109
|
+
if (ts.isSpreadAssignment(property)) {
|
|
110
|
+
return [{ name: undefined, value: property.expression, spread: true as const }]
|
|
111
|
+
}
|
|
112
|
+
// Methods and accessors create enumerable own properties too. Their
|
|
113
|
+
// values are dynamic to source analysis, but their names still make
|
|
114
|
+
// env/vars scopes complete when every key is known.
|
|
115
|
+
return 'name' in property ? [{ name: propertyKeyName(property.name), value: property }] : []
|
|
116
|
+
})
|
|
71
117
|
: undefined,
|
|
72
118
|
elements: node => (ts.isArrayLiteralExpression(node) ? [...node.elements] : undefined),
|
|
73
119
|
}
|
|
74
120
|
|
|
75
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Statically-known property key: `path` in `{ path: ... }`, `{ 'path': ... }`,
|
|
123
|
+
* or `{ ['path']: ... }`; undefined for other computed keys.
|
|
124
|
+
*/
|
|
76
125
|
function propertyKeyName(name: TS.PropertyName): string | undefined {
|
|
126
|
+
if (ts.isComputedPropertyName(name)) {
|
|
127
|
+
return ts.isStringLiteral(name.expression) ? name.expression.text : undefined
|
|
128
|
+
}
|
|
77
129
|
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined
|
|
78
130
|
}
|
|
79
131
|
|
|
@@ -118,7 +170,8 @@ export function createSiteFinder(ts: TypeScriptApi): SiteFinder {
|
|
|
118
170
|
/** Collects imports, engine locals, rebound names, and DTO class roots before extracting sites. */
|
|
119
171
|
function collectFile(
|
|
120
172
|
source: TS.SourceFile,
|
|
121
|
-
options: LocalModuleOptions
|
|
173
|
+
options: LocalModuleOptions,
|
|
174
|
+
engineSymbols: ReadonlySet<TS.Symbol>
|
|
122
175
|
): {
|
|
123
176
|
bindings: SourceBindings
|
|
124
177
|
dtoRoots: ReadonlyMap<string, string>
|
|
@@ -164,12 +217,20 @@ export function createSiteFinder(ts: TypeScriptApi): SiteFinder {
|
|
|
164
217
|
) {
|
|
165
218
|
const set = constructsEngine(node.initializer.expression.text, bindings) ? trusted : rebound
|
|
166
219
|
set.add(node.name.text)
|
|
220
|
+
} else if (ts.isIdentifier(node.name) && isEngineExpression(node.name, engineSymbols)) {
|
|
221
|
+
trusted.add(node.name.text)
|
|
222
|
+
foreign.delete(node.name.text)
|
|
167
223
|
} else {
|
|
168
224
|
// Catch clauses reach here too: `catch (r4)` is a VariableDeclaration.
|
|
169
225
|
addBindingNames(node.name, rebound)
|
|
170
226
|
}
|
|
171
227
|
} else if (ts.isParameter(node)) {
|
|
172
|
-
|
|
228
|
+
if (ts.isIdentifier(node.name) && isEngineExpression(node.name, engineSymbols)) {
|
|
229
|
+
trusted.add(node.name.text)
|
|
230
|
+
foreign.delete(node.name.text)
|
|
231
|
+
} else {
|
|
232
|
+
addBindingNames(node.name, rebound)
|
|
233
|
+
}
|
|
173
234
|
} else if (
|
|
174
235
|
(ts.isFunctionDeclaration(node) ||
|
|
175
236
|
ts.isFunctionExpression(node) ||
|
|
@@ -185,6 +246,115 @@ export function createSiteFinder(ts: TypeScriptApi): SiteFinder {
|
|
|
185
246
|
return { bindings, dtoRoots: dtoRootsOf([...heritage.values()]), heritage }
|
|
186
247
|
}
|
|
187
248
|
|
|
249
|
+
/** Package engine declarations reached through a real `fhirpath-ts` import. */
|
|
250
|
+
function collectTrustedEngineSymbols(options: LocalModuleOptions): ReadonlySet<TS.Symbol> {
|
|
251
|
+
const symbols = new Set<TS.Symbol>()
|
|
252
|
+
if (checker === undefined || program === undefined) {
|
|
253
|
+
return symbols
|
|
254
|
+
}
|
|
255
|
+
const resolvedSymbol = (initial: TS.Symbol | undefined): TS.Symbol | undefined => {
|
|
256
|
+
let symbol = initial
|
|
257
|
+
const seen = new Set<TS.Symbol>()
|
|
258
|
+
while (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0 && !seen.has(symbol)) {
|
|
259
|
+
seen.add(symbol)
|
|
260
|
+
symbol = checker.getAliasedSymbol(symbol)
|
|
261
|
+
}
|
|
262
|
+
return symbol
|
|
263
|
+
}
|
|
264
|
+
const resolved = (node: TS.Node): TS.Symbol | undefined => resolvedSymbol(checker.getSymbolAtLocation(node))
|
|
265
|
+
for (const source of program.getSourceFiles()) {
|
|
266
|
+
for (const statement of source.statements) {
|
|
267
|
+
if (
|
|
268
|
+
!ts.isImportDeclaration(statement) ||
|
|
269
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
270
|
+
isForeignModule(statement.moduleSpecifier.text, options)
|
|
271
|
+
) {
|
|
272
|
+
continue
|
|
273
|
+
}
|
|
274
|
+
const bindings = statement.importClause?.namedBindings
|
|
275
|
+
if (bindings === undefined) {
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
if (ts.isNamedImports(bindings)) {
|
|
279
|
+
for (const element of bindings.elements) {
|
|
280
|
+
if ((element.propertyName ?? element.name).text === ENGINE_CLASS_NAME) {
|
|
281
|
+
const symbol = resolved(element.name)
|
|
282
|
+
if (symbol !== undefined) {
|
|
283
|
+
symbols.add(symbol)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
const module = resolved(bindings.name)
|
|
290
|
+
const symbol =
|
|
291
|
+
module === undefined
|
|
292
|
+
? undefined
|
|
293
|
+
: checker.getExportsOfModule(module).find(entry => entry.name === ENGINE_CLASS_NAME)
|
|
294
|
+
if (symbol !== undefined) {
|
|
295
|
+
symbols.add(resolvedSymbol(symbol) ?? symbol)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return symbols
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function engineSymbolsFor(options: LocalModuleOptions): ReadonlySet<TS.Symbol> {
|
|
303
|
+
const key = JSON.stringify({ packages: options.packages, localImports: options.localImports === true })
|
|
304
|
+
const cached = engineSymbolsByOptions.get(key)
|
|
305
|
+
if (cached !== undefined) {
|
|
306
|
+
return cached
|
|
307
|
+
}
|
|
308
|
+
const symbols = collectTrustedEngineSymbols(options)
|
|
309
|
+
engineSymbolsByOptions.set(key, symbols)
|
|
310
|
+
return symbols
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Whether TypeScript resolved an expression to this package's engine class. */
|
|
314
|
+
function isEngineExpression(node: TS.Expression, engineSymbols: ReadonlySet<TS.Symbol>): boolean {
|
|
315
|
+
if (checker === undefined || engineSymbols.size === 0 || node.getSourceFile().isDeclarationFile) {
|
|
316
|
+
return false
|
|
317
|
+
}
|
|
318
|
+
const hasEngineSymbol = (type: TS.Type, seen: Set<TS.Type>): boolean => {
|
|
319
|
+
if (seen.has(type)) {
|
|
320
|
+
return false
|
|
321
|
+
}
|
|
322
|
+
seen.add(type)
|
|
323
|
+
if (type.isUnion()) {
|
|
324
|
+
return type.types.length > 0 && type.types.every(member => hasEngineSymbol(member, new Set(seen)))
|
|
325
|
+
}
|
|
326
|
+
if (type.isIntersection()) {
|
|
327
|
+
return type.types.some(member => hasEngineSymbol(member, new Set(seen)))
|
|
328
|
+
}
|
|
329
|
+
const symbol = type.aliasSymbol ?? type.getSymbol()
|
|
330
|
+
if (symbol !== undefined && engineSymbols.has(symbol)) {
|
|
331
|
+
return true
|
|
332
|
+
}
|
|
333
|
+
const constraint = checker.getBaseConstraintOfType(type)
|
|
334
|
+
if (constraint !== undefined && constraint !== type && hasEngineSymbol(constraint, seen)) {
|
|
335
|
+
return true
|
|
336
|
+
}
|
|
337
|
+
if ((type.flags & ts.TypeFlags.Object) === 0) {
|
|
338
|
+
return false
|
|
339
|
+
}
|
|
340
|
+
const object = type as TS.ObjectType
|
|
341
|
+
if ((object.objectFlags & (ts.ObjectFlags.Class | ts.ObjectFlags.Interface | ts.ObjectFlags.Reference)) === 0) {
|
|
342
|
+
return false
|
|
343
|
+
}
|
|
344
|
+
return checker.getBaseTypes(type as TS.InterfaceType).some(base => hasEngineSymbol(base, seen))
|
|
345
|
+
}
|
|
346
|
+
return hasEngineSymbol(checker.getTypeAtLocation(node), new Set())
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** An unresolved receiver may still be an engine; a resolved non-engine is not our call site. */
|
|
350
|
+
function shouldReportUnrecognized(node: TS.Expression): boolean {
|
|
351
|
+
if (checker === undefined || node.getSourceFile().isDeclarationFile) {
|
|
352
|
+
return true
|
|
353
|
+
}
|
|
354
|
+
const flags = checker.getTypeAtLocation(node).flags
|
|
355
|
+
return (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0
|
|
356
|
+
}
|
|
357
|
+
|
|
188
358
|
/** Add every identifier a binding name declares (`x`, `{ r4 }`, `[a, ...rest]`). */
|
|
189
359
|
function addBindingNames(name: TS.BindingName, into: Set<string>): void {
|
|
190
360
|
if (ts.isIdentifier(name)) {
|
|
@@ -220,14 +390,129 @@ export function createSiteFinder(ts: TypeScriptApi): SiteFinder {
|
|
|
220
390
|
return ts.isIdentifier(current) ? current.text : undefined
|
|
221
391
|
}
|
|
222
392
|
|
|
223
|
-
|
|
393
|
+
/** Strips source wrappers that keep an expression's runtime identity. */
|
|
394
|
+
function unwrapped(node: TS.Expression | undefined): TS.Expression | undefined {
|
|
395
|
+
let current = node
|
|
396
|
+
while (
|
|
397
|
+
current !== undefined &&
|
|
398
|
+
(ts.isParenthesizedExpression(current) ||
|
|
399
|
+
ts.isAsExpression(current) ||
|
|
400
|
+
ts.isSatisfiesExpression(current) ||
|
|
401
|
+
ts.isTypeAssertionExpression(current) ||
|
|
402
|
+
ts.isNonNullExpression(current))
|
|
403
|
+
) {
|
|
404
|
+
current = current.expression
|
|
405
|
+
}
|
|
406
|
+
return current
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Class bindings made reachable by one immutable initializer or default export. */
|
|
410
|
+
function bindingTargets(node: TS.Expression | undefined): string[] {
|
|
411
|
+
const target = unwrapped(node)
|
|
412
|
+
if (target === undefined) {
|
|
413
|
+
return []
|
|
414
|
+
}
|
|
415
|
+
if (ts.isIdentifier(target)) {
|
|
416
|
+
return [target.text]
|
|
417
|
+
}
|
|
418
|
+
if (!ts.isClassExpression(target)) {
|
|
419
|
+
return []
|
|
420
|
+
}
|
|
421
|
+
// A class expression's own name is scoped to its body, not a reference to a
|
|
422
|
+
// same-named top-level class. Only its base can expose another declaration.
|
|
423
|
+
const { baseName } = heritageOf(target)
|
|
424
|
+
return baseName === undefined ? [] : [baseName]
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The module's exported names, plus what each top-level `const` binding stands
|
|
429
|
+
* for: `export const Row = ProblemRow` makes the loader enumerate `Row`, so
|
|
430
|
+
* `ProblemRow` is reachable through it — directly, through an alias chain, or
|
|
431
|
+
* as the base of an exported anonymous class expression.
|
|
432
|
+
*/
|
|
433
|
+
function moduleBindings(source: TS.SourceFile): {
|
|
434
|
+
exported: ReadonlySet<string>
|
|
435
|
+
aliases: ReadonlyMap<string, readonly string[]>
|
|
436
|
+
} {
|
|
437
|
+
const exported = new Set<string>()
|
|
438
|
+
const aliases = new Map<string, string[]>()
|
|
439
|
+
const addAlias = (from: string, to: string): void => {
|
|
440
|
+
const edges = aliases.get(from)
|
|
441
|
+
if (edges === undefined) {
|
|
442
|
+
aliases.set(from, [to])
|
|
443
|
+
} else {
|
|
444
|
+
edges.push(to)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
const isExported = (statement: TS.Statement): boolean =>
|
|
448
|
+
(ts.canHaveModifiers(statement) ? (ts.getModifiers(statement) ?? []) : []).some(
|
|
449
|
+
modifier => modifier.kind === ts.SyntaxKind.ExportKeyword
|
|
450
|
+
)
|
|
451
|
+
for (const statement of source.statements) {
|
|
452
|
+
if (ts.isClassDeclaration(statement) && isExported(statement)) {
|
|
453
|
+
if (statement.name !== undefined) {
|
|
454
|
+
exported.add(statement.name.text)
|
|
455
|
+
} else {
|
|
456
|
+
// `export default class extends Base {}`: the loader gets the class,
|
|
457
|
+
// so its base is reachable even though neither has a bound name here.
|
|
458
|
+
const base = heritageOf(statement).baseName
|
|
459
|
+
if (base !== undefined) {
|
|
460
|
+
exported.add(base)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
} else if (ts.isVariableStatement(statement)) {
|
|
464
|
+
const immutable = (statement.declarationList.flags & ts.NodeFlags.Const) !== 0
|
|
465
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
466
|
+
if (!ts.isIdentifier(declaration.name)) {
|
|
467
|
+
continue
|
|
468
|
+
}
|
|
469
|
+
const name = declaration.name.text
|
|
470
|
+
if (isExported(statement)) {
|
|
471
|
+
exported.add(name)
|
|
472
|
+
}
|
|
473
|
+
if (immutable) {
|
|
474
|
+
for (const target of bindingTargets(declaration.initializer)) {
|
|
475
|
+
addAlias(name, target)
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
} else if (
|
|
480
|
+
ts.isExportDeclaration(statement) &&
|
|
481
|
+
statement.moduleSpecifier === undefined &&
|
|
482
|
+
statement.isTypeOnly === false &&
|
|
483
|
+
statement.exportClause !== undefined
|
|
484
|
+
) {
|
|
485
|
+
if (ts.isNamedExports(statement.exportClause)) {
|
|
486
|
+
for (const element of statement.exportClause.elements) {
|
|
487
|
+
if (!element.isTypeOnly) {
|
|
488
|
+
exported.add((element.propertyName ?? element.name).text)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
} else if (ts.isExportAssignment(statement)) {
|
|
493
|
+
for (const target of bindingTargets(statement.expression)) {
|
|
494
|
+
exported.add(target)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return { exported, aliases }
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return function scanExpressionSites(
|
|
224
502
|
sourceText: string,
|
|
225
503
|
fileName: string,
|
|
226
504
|
options: LocalModuleOptions = {}
|
|
227
|
-
):
|
|
228
|
-
const
|
|
229
|
-
const
|
|
505
|
+
): SiteScanResult {
|
|
506
|
+
const programSource = program?.getSourceFile(fileName)
|
|
507
|
+
const source =
|
|
508
|
+
programSource !== undefined && programSource.text === sourceText
|
|
509
|
+
? programSource
|
|
510
|
+
: ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true)
|
|
511
|
+
const engineSymbols = engineSymbolsFor(options)
|
|
512
|
+
const { bindings, dtoRoots, heritage } = collectFile(source, options, engineSymbols)
|
|
230
513
|
const sites: ExpressionSite[] = []
|
|
514
|
+
const skipped: SkippedExpressionSite[] = []
|
|
515
|
+
const decoratedClasses = new Set<TS.ClassLikeDeclaration>()
|
|
231
516
|
const functions: Record<string, FileColumnFunction> = {}
|
|
232
517
|
// A site points at the first character inside the quote/backtick (node start
|
|
233
518
|
// + 1), so fhirpath-check can add a diagnostic's span offsets directly. The
|
|
@@ -243,43 +528,146 @@ export function createSiteFinder(ts: TypeScriptApi): SiteFinder {
|
|
|
243
528
|
...context,
|
|
244
529
|
})
|
|
245
530
|
}
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
531
|
+
const skip = (node: TS.Node, reason: SkippedExpressionSite['reason'], message: string): void => {
|
|
532
|
+
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source))
|
|
533
|
+
skipped.push({ reason, message, line: line + 1, column: character + 1 })
|
|
534
|
+
}
|
|
535
|
+
const visit = (
|
|
536
|
+
node: TS.Node,
|
|
537
|
+
classRoot: string | undefined,
|
|
538
|
+
enclosingClass: TS.ClassLikeDeclaration | undefined
|
|
539
|
+
): void => {
|
|
540
|
+
if (ts.isTaggedTemplateExpression(node) && nameOf(node.tag) === TAG_NAME) {
|
|
541
|
+
if (!isCheckedTag(receiverRoot(node.tag), bindings)) {
|
|
542
|
+
if (shouldReportUnrecognized(node.tag)) {
|
|
543
|
+
skip(node.tag, 'unrecognized-receiver', 'FHIRPath tag not analyzed: its binding is not from fhirpath-ts')
|
|
544
|
+
}
|
|
545
|
+
} else if (ts.isNoSubstitutionTemplateLiteral(node.template)) {
|
|
253
546
|
record(node.template.text, node.template)
|
|
547
|
+
} else {
|
|
548
|
+
skip(node.template, 'dynamic-expression', 'FHIRPath template not analyzed: substitutions make it dynamic')
|
|
254
549
|
}
|
|
255
550
|
} else if (ts.isCallExpression(node)) {
|
|
256
551
|
const callee = nameOf(node.expression)
|
|
257
552
|
const policy = callee === undefined ? undefined : CALL_SITES.get(callee)
|
|
258
553
|
const argument = policy && (node.arguments[policy.argIndex] as TS.Expression | undefined)
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
isCheckedCall(policy, callee, receiverRoot(node.expression), bindings
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
554
|
+
if (callee !== undefined && policy !== undefined && argument !== undefined) {
|
|
555
|
+
const typedEngineReceiver =
|
|
556
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
557
|
+
isEngineExpression(node.expression.expression, engineSymbols)
|
|
558
|
+
const checked = isCheckedCall(policy, callee, receiverRoot(node.expression), bindings, {
|
|
559
|
+
...(typedEngineReceiver && { engine: true }),
|
|
560
|
+
})
|
|
561
|
+
if (!checked) {
|
|
562
|
+
const receiver = receiverRoot(node.expression)
|
|
563
|
+
const receiverExpression = ts.isPropertyAccessExpression(node.expression)
|
|
564
|
+
? node.expression.expression
|
|
565
|
+
: node.expression
|
|
566
|
+
if (shouldReportUnrecognized(receiverExpression)) {
|
|
567
|
+
skip(
|
|
568
|
+
node.expression,
|
|
569
|
+
'unrecognized-receiver',
|
|
570
|
+
`${callee}(...) expression not analyzed: ${
|
|
571
|
+
receiver === undefined
|
|
572
|
+
? `binding '${callee}' is not recognized as fhirpath-ts`
|
|
573
|
+
: `receiver '${receiver}' is not recognized as a FhirPathEngine`
|
|
574
|
+
}`
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
} else {
|
|
578
|
+
const declares = policy.declaresField
|
|
579
|
+
const field = declares === undefined ? undefined : decoratedFieldName(node)
|
|
580
|
+
if (declares !== undefined && field !== undefined) {
|
|
581
|
+
if (enclosingClass !== undefined) {
|
|
582
|
+
decoratedClasses.add(enclosingClass)
|
|
583
|
+
}
|
|
584
|
+
functions[field] = declaredColumnOverloads(
|
|
585
|
+
functions[field],
|
|
586
|
+
columnFunctionDeclaration<TS.Node>(declares, node.arguments[1], tsAst, classRoot)
|
|
587
|
+
)
|
|
588
|
+
}
|
|
589
|
+
for (const candidate of callExpressionCandidates<TS.Node>(
|
|
590
|
+
policy,
|
|
591
|
+
index => node.arguments[index],
|
|
592
|
+
classRoot,
|
|
593
|
+
tsAst
|
|
594
|
+
)) {
|
|
595
|
+
if (candidate.uncheckable === 'dynamic-vars') {
|
|
596
|
+
skip(
|
|
597
|
+
candidate.node,
|
|
598
|
+
'dynamic-expression',
|
|
599
|
+
`${callee}(...) var expressions not analyzed: their final names, values, or order are dynamic`
|
|
600
|
+
)
|
|
601
|
+
} else if (candidate.expression === undefined) {
|
|
602
|
+
skip(
|
|
603
|
+
candidate.node,
|
|
604
|
+
'dynamic-expression',
|
|
605
|
+
candidate.source === 'option-var'
|
|
606
|
+
? `${callee}(...) var expression not analyzed: it is not a string literal`
|
|
607
|
+
: `${callee}(...) expression not analyzed: it is not a string literal`
|
|
608
|
+
)
|
|
609
|
+
} else {
|
|
610
|
+
record(candidate.expression, candidate.node, candidate.context)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
276
613
|
}
|
|
277
614
|
}
|
|
278
615
|
}
|
|
279
616
|
const nested = ts.isClassLike(node) ? rootOf(heritage.get(node), dtoRoots) : classRoot
|
|
280
|
-
ts.
|
|
617
|
+
const nestedClass = ts.isClassLike(node) ? node : enclosingClass
|
|
618
|
+
ts.forEachChild(node, child => visit(child, nested, nestedClass))
|
|
619
|
+
}
|
|
620
|
+
visit(source, undefined, undefined)
|
|
621
|
+
const { exported, aliases } = moduleBindings(source)
|
|
622
|
+
const classByName = new Map(
|
|
623
|
+
[...heritage.entries()].flatMap(([node, value]) =>
|
|
624
|
+
ts.isClassLike(node) && value.name !== undefined ? [[value.name, value] as const] : []
|
|
625
|
+
)
|
|
626
|
+
)
|
|
627
|
+
// From every exported binding, follow alias and extends edges: what the
|
|
628
|
+
// loader enumerates under any name makes every class behind it analyzable.
|
|
629
|
+
const reachesExport = (name: string): boolean => {
|
|
630
|
+
const seen = new Set<string>()
|
|
631
|
+
const frontier = [...exported]
|
|
632
|
+
while (frontier.length > 0) {
|
|
633
|
+
const current = frontier.pop() as string
|
|
634
|
+
if (current === name) {
|
|
635
|
+
return true
|
|
636
|
+
}
|
|
637
|
+
if (seen.has(current)) {
|
|
638
|
+
continue
|
|
639
|
+
}
|
|
640
|
+
seen.add(current)
|
|
641
|
+
const base = classByName.get(current)?.baseName
|
|
642
|
+
if (base !== undefined) {
|
|
643
|
+
frontier.push(base)
|
|
644
|
+
}
|
|
645
|
+
frontier.push(...(aliases.get(current) ?? []))
|
|
646
|
+
}
|
|
647
|
+
return false
|
|
648
|
+
}
|
|
649
|
+
const dtoDeclarations = [...decoratedClasses].flatMap(node => {
|
|
650
|
+
const nameNode = node.name
|
|
651
|
+
// Runtime module discovery can only miss top-level module-local classes.
|
|
652
|
+
// A class inside a factory is reached through whatever class the factory
|
|
653
|
+
// returns, rather than as a module export with its source name.
|
|
654
|
+
if (nameNode === undefined || node.parent !== source) {
|
|
655
|
+
return []
|
|
656
|
+
}
|
|
657
|
+
const name = nameNode.text
|
|
658
|
+
const { line, character } = source.getLineAndCharacterOfPosition(nameNode.getStart(source))
|
|
659
|
+
return [{ name, line: line + 1, column: character + 1, loadable: reachesExport(name) }]
|
|
660
|
+
})
|
|
661
|
+
return {
|
|
662
|
+
sites: Object.keys(functions).length === 0 ? sites : sites.map(site => ({ ...site, functions })),
|
|
663
|
+
skipped,
|
|
664
|
+
dtoDeclarations,
|
|
281
665
|
}
|
|
282
|
-
visit(source, undefined)
|
|
283
|
-
return Object.keys(functions).length === 0 ? sites : sites.map(site => ({ ...site, functions }))
|
|
284
666
|
}
|
|
285
667
|
}
|
|
668
|
+
|
|
669
|
+
/** Creates the compatibility finder that returns only expressions successfully extracted. */
|
|
670
|
+
export function createSiteFinder(ts: TypeScriptApi, program?: TS.Program): SiteFinder {
|
|
671
|
+
const scan = createSiteScanner(ts, program)
|
|
672
|
+
return (sourceText, fileName, options) => scan(sourceText, fileName, options).sites
|
|
673
|
+
}
|
package/src/typed/infer.ts
CHANGED
|
@@ -32,9 +32,9 @@ export type FhirpathTypeDeclarations = Readonly<Record<string, FhirpathTypeDecla
|
|
|
32
32
|
export type FhirpathFunctionDeclaration =
|
|
33
33
|
| {
|
|
34
34
|
readonly signature?: {
|
|
35
|
-
readonly input?: { readonly types?: readonly string[] }
|
|
35
|
+
readonly input?: { readonly types?: readonly string[]; readonly ordered?: boolean }
|
|
36
36
|
readonly args?: readonly string[]
|
|
37
|
-
readonly result?: { readonly types?: readonly string[]; readonly single?: boolean }
|
|
37
|
+
readonly result?: { readonly types?: readonly string[]; readonly single?: boolean; readonly ordered?: boolean }
|
|
38
38
|
}
|
|
39
39
|
readonly expression?: string | { readonly source: string }
|
|
40
40
|
readonly criteria?: boolean
|