fhirpath-ts 0.0.0-bootstrap.0
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/LICENSE +201 -0
- package/README.md +391 -0
- package/THIRD-PARTY-NOTICES.md +296 -0
- package/dist/analyzer/analyze-dto.d.ts +45 -0
- package/dist/analyzer/analyze-dto.d.ts.map +1 -0
- package/dist/analyzer/analyze-dto.js +147 -0
- package/dist/analyzer/analyze-dto.js.map +1 -0
- package/dist/analyzer/analyze.d.ts +98 -0
- package/dist/analyzer/analyze.d.ts.map +1 -0
- package/dist/analyzer/analyze.js +925 -0
- package/dist/analyzer/analyze.js.map +1 -0
- package/dist/analyzer/expression-policy.d.ts +210 -0
- package/dist/analyzer/expression-policy.d.ts.map +1 -0
- package/dist/analyzer/expression-policy.js +254 -0
- package/dist/analyzer/expression-policy.js.map +1 -0
- package/dist/analyzer/index.d.ts +3 -0
- package/dist/analyzer/index.d.ts.map +1 -0
- package/dist/analyzer/index.js +6 -0
- package/dist/analyzer/index.js.map +1 -0
- package/dist/analyzer/regex-safety.d.ts +10 -0
- package/dist/analyzer/regex-safety.d.ts.map +1 -0
- package/dist/analyzer/regex-safety.js +84 -0
- package/dist/analyzer/regex-safety.js.map +1 -0
- package/dist/analyzer/signatures.d.ts +72 -0
- package/dist/analyzer/signatures.d.ts.map +1 -0
- package/dist/analyzer/signatures.js +195 -0
- package/dist/analyzer/signatures.js.map +1 -0
- package/dist/api/bundle.d.ts +29 -0
- package/dist/api/bundle.d.ts.map +1 -0
- package/dist/api/bundle.js +108 -0
- package/dist/api/bundle.js.map +1 -0
- package/dist/api/cache.d.ts +10 -0
- package/dist/api/cache.d.ts.map +1 -0
- package/dist/api/cache.js +38 -0
- package/dist/api/cache.js.map +1 -0
- package/dist/api/column-signature.d.ts +53 -0
- package/dist/api/column-signature.d.ts.map +1 -0
- package/dist/api/column-signature.js +43 -0
- package/dist/api/column-signature.js.map +1 -0
- package/dist/api/compile.d.ts +120 -0
- package/dist/api/compile.d.ts.map +1 -0
- package/dist/api/compile.js +167 -0
- package/dist/api/compile.js.map +1 -0
- package/dist/api/constraints.d.ts +48 -0
- package/dist/api/constraints.d.ts.map +1 -0
- package/dist/api/constraints.js +52 -0
- package/dist/api/constraints.js.map +1 -0
- package/dist/api/dto.d.ts +134 -0
- package/dist/api/dto.d.ts.map +1 -0
- package/dist/api/dto.js +295 -0
- package/dist/api/dto.js.map +1 -0
- package/dist/api/engine.d.ts +136 -0
- package/dist/api/engine.d.ts.map +1 -0
- package/dist/api/engine.js +183 -0
- package/dist/api/engine.js.map +1 -0
- package/dist/api/evaluate.d.ts +7 -0
- package/dist/api/evaluate.d.ts.map +1 -0
- package/dist/api/evaluate.js +10 -0
- package/dist/api/evaluate.js.map +1 -0
- package/dist/api/project.d.ts +89 -0
- package/dist/api/project.d.ts.map +1 -0
- package/dist/api/project.js +150 -0
- package/dist/api/project.js.map +1 -0
- package/dist/api/tagged.d.ts +14 -0
- package/dist/api/tagged.d.ts.map +1 -0
- package/dist/api/tagged.js +13 -0
- package/dist/api/tagged.js.map +1 -0
- package/dist/cli/dto-check.d.ts +25 -0
- package/dist/cli/dto-check.d.ts.map +1 -0
- package/dist/cli/dto-check.js +77 -0
- package/dist/cli/dto-check.js.map +1 -0
- package/dist/cli/fhirpath-check.d.ts +3 -0
- package/dist/cli/fhirpath-check.d.ts.map +1 -0
- package/dist/cli/fhirpath-check.js +149 -0
- package/dist/cli/fhirpath-check.js.map +1 -0
- package/dist/cli/ts-loader.d.mts +3 -0
- package/dist/cli/ts-loader.d.mts.map +1 -0
- package/dist/cli/ts-loader.mjs +48 -0
- package/dist/cli/ts-loader.mjs.map +1 -0
- package/dist/engine/context.d.ts +173 -0
- package/dist/engine/context.d.ts.map +1 -0
- package/dist/engine/context.js +118 -0
- package/dist/engine/context.js.map +1 -0
- package/dist/engine/evaluator.d.ts +7 -0
- package/dist/engine/evaluator.d.ts.map +1 -0
- package/dist/engine/evaluator.js +196 -0
- package/dist/engine/evaluator.js.map +1 -0
- package/dist/engine/navigation.d.ts +11 -0
- package/dist/engine/navigation.d.ts.map +1 -0
- package/dist/engine/navigation.js +102 -0
- package/dist/engine/navigation.js.map +1 -0
- package/dist/engine/operators/collections.d.ts +12 -0
- package/dist/engine/operators/collections.d.ts.map +1 -0
- package/dist/engine/operators/collections.js +31 -0
- package/dist/engine/operators/collections.js.map +1 -0
- package/dist/engine/operators/comparison.d.ts +13 -0
- package/dist/engine/operators/comparison.d.ts.map +1 -0
- package/dist/engine/operators/comparison.js +77 -0
- package/dist/engine/operators/comparison.js.map +1 -0
- package/dist/engine/operators/equality.d.ts +19 -0
- package/dist/engine/operators/equality.d.ts.map +1 -0
- package/dist/engine/operators/equality.js +203 -0
- package/dist/engine/operators/equality.js.map +1 -0
- package/dist/engine/operators/index.d.ts +18 -0
- package/dist/engine/operators/index.d.ts.map +1 -0
- package/dist/engine/operators/index.js +30 -0
- package/dist/engine/operators/index.js.map +1 -0
- package/dist/engine/operators/logic.d.ts +8 -0
- package/dist/engine/operators/logic.d.ts.map +1 -0
- package/dist/engine/operators/logic.js +38 -0
- package/dist/engine/operators/logic.js.map +1 -0
- package/dist/engine/operators/math.d.ts +13 -0
- package/dist/engine/operators/math.d.ts.map +1 -0
- package/dist/engine/operators/math.js +198 -0
- package/dist/engine/operators/math.js.map +1 -0
- package/dist/engine/operators/strings.d.ts +8 -0
- package/dist/engine/operators/strings.d.ts.map +1 -0
- package/dist/engine/operators/strings.js +22 -0
- package/dist/engine/operators/strings.js.map +1 -0
- package/dist/engine/operators/types.d.ts +3 -0
- package/dist/engine/operators/types.d.ts.map +1 -0
- package/dist/engine/operators/types.js +15 -0
- package/dist/engine/operators/types.js.map +1 -0
- package/dist/engine/type-matching.d.ts +20 -0
- package/dist/engine/type-matching.d.ts.map +1 -0
- package/dist/engine/type-matching.js +122 -0
- package/dist/engine/type-matching.js.map +1 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +21 -0
- package/dist/errors.js.map +1 -0
- package/dist/eslint/index.d.ts +12 -0
- package/dist/eslint/index.d.ts.map +1 -0
- package/dist/eslint/index.js +316 -0
- package/dist/eslint/index.js.map +1 -0
- package/dist/fhir/html-checks.d.ts +7 -0
- package/dist/fhir/html-checks.d.ts.map +1 -0
- package/dist/fhir/html-checks.js +256 -0
- package/dist/fhir/html-checks.js.map +1 -0
- package/dist/fhir/model-navigation.d.ts +11 -0
- package/dist/fhir/model-navigation.d.ts.map +1 -0
- package/dist/fhir/model-navigation.js +142 -0
- package/dist/fhir/model-navigation.js.map +1 -0
- package/dist/functions/aggregate.d.ts +2 -0
- package/dist/functions/aggregate.d.ts.map +1 -0
- package/dist/functions/aggregate.js +127 -0
- package/dist/functions/aggregate.js.map +1 -0
- package/dist/functions/boundary.d.ts +2 -0
- package/dist/functions/boundary.d.ts.map +1 -0
- package/dist/functions/boundary.js +189 -0
- package/dist/functions/boundary.js.map +1 -0
- package/dist/functions/combining.d.ts +2 -0
- package/dist/functions/combining.d.ts.map +1 -0
- package/dist/functions/combining.js +13 -0
- package/dist/functions/combining.js.map +1 -0
- package/dist/functions/control.d.ts +2 -0
- package/dist/functions/control.d.ts.map +1 -0
- package/dist/functions/control.js +25 -0
- package/dist/functions/control.js.map +1 -0
- package/dist/functions/conversion.d.ts +3 -0
- package/dist/functions/conversion.d.ts.map +1 -0
- package/dist/functions/conversion.js +316 -0
- package/dist/functions/conversion.js.map +1 -0
- package/dist/functions/date-components.d.ts +2 -0
- package/dist/functions/date-components.d.ts.map +1 -0
- package/dist/functions/date-components.js +81 -0
- package/dist/functions/date-components.js.map +1 -0
- package/dist/functions/existence.d.ts +4 -0
- package/dist/functions/existence.d.ts.map +1 -0
- package/dist/functions/existence.js +97 -0
- package/dist/functions/existence.js.map +1 -0
- package/dist/functions/fhir.d.ts +2 -0
- package/dist/functions/fhir.d.ts.map +1 -0
- package/dist/functions/fhir.js +222 -0
- package/dist/functions/fhir.js.map +1 -0
- package/dist/functions/filtering.d.ts +2 -0
- package/dist/functions/filtering.d.ts.map +1 -0
- package/dist/functions/filtering.js +112 -0
- package/dist/functions/filtering.js.map +1 -0
- package/dist/functions/install.d.ts +18 -0
- package/dist/functions/install.d.ts.map +1 -0
- package/dist/functions/install.js +20 -0
- package/dist/functions/install.js.map +1 -0
- package/dist/functions/iteration.d.ts +7 -0
- package/dist/functions/iteration.d.ts.map +1 -0
- package/dist/functions/iteration.js +9 -0
- package/dist/functions/iteration.js.map +1 -0
- package/dist/functions/math.d.ts +2 -0
- package/dist/functions/math.d.ts.map +1 -0
- package/dist/functions/math.js +152 -0
- package/dist/functions/math.js.map +1 -0
- package/dist/functions/reflection.d.ts +2 -0
- package/dist/functions/reflection.d.ts.map +1 -0
- package/dist/functions/reflection.js +23 -0
- package/dist/functions/reflection.js.map +1 -0
- package/dist/functions/registry.d.ts +28 -0
- package/dist/functions/registry.d.ts.map +1 -0
- package/dist/functions/registry.js +46 -0
- package/dist/functions/registry.js.map +1 -0
- package/dist/functions/sort.d.ts +2 -0
- package/dist/functions/sort.d.ts.map +1 -0
- package/dist/functions/sort.js +48 -0
- package/dist/functions/sort.js.map +1 -0
- package/dist/functions/string.d.ts +2 -0
- package/dist/functions/string.d.ts.map +1 -0
- package/dist/functions/string.js +353 -0
- package/dist/functions/string.js.map +1 -0
- package/dist/functions/subsetting.d.ts +2 -0
- package/dist/functions/subsetting.d.ts.map +1 -0
- package/dist/functions/subsetting.js +72 -0
- package/dist/functions/subsetting.js.map +1 -0
- package/dist/functions/tree.d.ts +8 -0
- package/dist/functions/tree.d.ts.map +1 -0
- package/dist/functions/tree.js +109 -0
- package/dist/functions/tree.js.map +1 -0
- package/dist/functions/type-specifier.d.ts +8 -0
- package/dist/functions/type-specifier.d.ts.map +1 -0
- package/dist/functions/type-specifier.js +27 -0
- package/dist/functions/type-specifier.js.map +1 -0
- package/dist/functions/utility.d.ts +2 -0
- package/dist/functions/utility.d.ts.map +1 -0
- package/dist/functions/utility.js +76 -0
- package/dist/functions/utility.js.map +1 -0
- package/dist/functions/variables.d.ts +2 -0
- package/dist/functions/variables.d.ts.map +1 -0
- package/dist/functions/variables.js +35 -0
- package/dist/functions/variables.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer/lexer.d.ts +4 -0
- package/dist/lexer/lexer.d.ts.map +1 -0
- package/dist/lexer/lexer.js +318 -0
- package/dist/lexer/lexer.js.map +1 -0
- package/dist/lexer/tokens.d.ts +23 -0
- package/dist/lexer/tokens.d.ts.map +1 -0
- package/dist/lexer/tokens.js +40 -0
- package/dist/lexer/tokens.js.map +1 -0
- package/dist/model/provider.d.ts +36 -0
- package/dist/model/provider.d.ts.map +1 -0
- package/dist/model/provider.js +2 -0
- package/dist/model/provider.js.map +1 -0
- package/dist/parser/ast.d.ts +104 -0
- package/dist/parser/ast.d.ts.map +1 -0
- package/dist/parser/ast.js +2 -0
- package/dist/parser/ast.js.map +1 -0
- package/dist/parser/parser.d.ts +4 -0
- package/dist/parser/parser.d.ts.map +1 -0
- package/dist/parser/parser.js +299 -0
- package/dist/parser/parser.js.map +1 -0
- package/dist/parser/precedence.d.ts +24 -0
- package/dist/parser/precedence.d.ts.map +1 -0
- package/dist/parser/precedence.js +52 -0
- package/dist/parser/precedence.js.map +1 -0
- package/dist/parser/printer.d.ts +4 -0
- package/dist/parser/printer.d.ts.map +1 -0
- package/dist/parser/printer.js +112 -0
- package/dist/parser/printer.js.map +1 -0
- package/dist/r4/generated/resources-data.d.ts +3 -0
- package/dist/r4/generated/resources-data.d.ts.map +1 -0
- package/dist/r4/generated/resources-data.js +28628 -0
- package/dist/r4/generated/resources-data.js.map +1 -0
- package/dist/r4/generated/type-maps.d.ts +43153 -0
- package/dist/r4/generated/type-maps.d.ts.map +1 -0
- package/dist/r4/generated/type-maps.js +4 -0
- package/dist/r4/generated/type-maps.js.map +1 -0
- package/dist/r4/generated/types-data.d.ts +3 -0
- package/dist/r4/generated/types-data.d.ts.map +1 -0
- package/dist/r4/generated/types-data.js +2205 -0
- package/dist/r4/generated/types-data.js.map +1 -0
- package/dist/r4/index.d.ts +9 -0
- package/dist/r4/index.d.ts.map +1 -0
- package/dist/r4/index.js +94 -0
- package/dist/r4/index.js.map +1 -0
- package/dist/r4/model-data.d.ts +17 -0
- package/dist/r4/model-data.d.ts.map +1 -0
- package/dist/r4/model-data.js +2 -0
- package/dist/r4/model-data.js.map +1 -0
- package/dist/sites/index.d.ts +31 -0
- package/dist/sites/index.d.ts.map +1 -0
- package/dist/sites/index.js +205 -0
- package/dist/sites/index.js.map +1 -0
- package/dist/typed/infer.d.ts +234 -0
- package/dist/typed/infer.d.ts.map +1 -0
- package/dist/typed/infer.js +85 -0
- package/dist/typed/infer.js.map +1 -0
- package/dist/typed/perf-fixture.types.d.ts +136 -0
- package/dist/typed/perf-fixture.types.d.ts.map +1 -0
- package/dist/typed/perf-fixture.types.js +2 -0
- package/dist/typed/perf-fixture.types.js.map +1 -0
- package/dist/values/collection.d.ts +24 -0
- package/dist/values/collection.d.ts.map +1 -0
- package/dist/values/collection.js +51 -0
- package/dist/values/collection.js.map +1 -0
- package/dist/values/datetime.d.ts +54 -0
- package/dist/values/datetime.d.ts.map +1 -0
- package/dist/values/datetime.js +226 -0
- package/dist/values/datetime.js.map +1 -0
- package/dist/values/decimal.d.ts +48 -0
- package/dist/values/decimal.d.ts.map +1 -0
- package/dist/values/decimal.js +188 -0
- package/dist/values/decimal.js.map +1 -0
- package/dist/values/numeric.d.ts +24 -0
- package/dist/values/numeric.d.ts.map +1 -0
- package/dist/values/numeric.js +53 -0
- package/dist/values/numeric.js.map +1 -0
- package/dist/values/quantity.d.ts +49 -0
- package/dist/values/quantity.d.ts.map +1 -0
- package/dist/values/quantity.js +297 -0
- package/dist/values/quantity.js.map +1 -0
- package/dist/values/temporal-arithmetic.d.ts +17 -0
- package/dist/values/temporal-arithmetic.d.ts.map +1 -0
- package/dist/values/temporal-arithmetic.js +183 -0
- package/dist/values/temporal-arithmetic.js.map +1 -0
- package/dist/values/temporal-compare.d.ts +9 -0
- package/dist/values/temporal-compare.d.ts.map +1 -0
- package/dist/values/temporal-compare.js +59 -0
- package/dist/values/temporal-compare.js.map +1 -0
- package/dist/values/type-compat.d.ts +47 -0
- package/dist/values/type-compat.d.ts.map +1 -0
- package/dist/values/type-compat.js +100 -0
- package/dist/values/type-compat.js.map +1 -0
- package/dist/values/typed-value.d.ts +48 -0
- package/dist/values/typed-value.d.ts.map +1 -0
- package/dist/values/typed-value.js +117 -0
- package/dist/values/typed-value.js.map +1 -0
- package/dist/values/ucum.d.ts +14 -0
- package/dist/values/ucum.d.ts.map +1 -0
- package/dist/values/ucum.js +146 -0
- package/dist/values/ucum.js.map +1 -0
- package/package.json +137 -0
- package/src/analyzer/analyze-dto.ts +195 -0
- package/src/analyzer/analyze.ts +1181 -0
- package/src/analyzer/expression-policy.ts +434 -0
- package/src/analyzer/index.ts +23 -0
- package/src/analyzer/regex-safety.ts +85 -0
- package/src/analyzer/signatures.ts +260 -0
- package/src/api/bundle.ts +132 -0
- package/src/api/cache.ts +40 -0
- package/src/api/column-signature.ts +71 -0
- package/src/api/compile.ts +308 -0
- package/src/api/constraints.ts +104 -0
- package/src/api/dto.ts +445 -0
- package/src/api/engine.ts +328 -0
- package/src/api/evaluate.ts +11 -0
- package/src/api/project.ts +255 -0
- package/src/api/tagged.ts +35 -0
- package/src/cli/dto-check.ts +94 -0
- package/src/cli/fhirpath-check.ts +168 -0
- package/src/cli/ts-loader.mjs +49 -0
- package/src/engine/context.ts +282 -0
- package/src/engine/evaluator.ts +246 -0
- package/src/engine/navigation.ts +106 -0
- package/src/engine/operators/collections.ts +34 -0
- package/src/engine/operators/comparison.ts +82 -0
- package/src/engine/operators/equality.ts +232 -0
- package/src/engine/operators/index.ts +65 -0
- package/src/engine/operators/logic.ts +53 -0
- package/src/engine/operators/math.ts +225 -0
- package/src/engine/operators/strings.ts +25 -0
- package/src/engine/operators/types.ts +16 -0
- package/src/engine/type-matching.ts +148 -0
- package/src/errors.ts +33 -0
- package/src/eslint/index.ts +388 -0
- package/src/fhir/html-checks.ts +266 -0
- package/src/fhir/model-navigation.ts +148 -0
- package/src/functions/aggregate.ts +136 -0
- package/src/functions/boundary.ts +209 -0
- package/src/functions/combining.ts +15 -0
- package/src/functions/control.ts +25 -0
- package/src/functions/conversion.ts +349 -0
- package/src/functions/date-components.ts +88 -0
- package/src/functions/existence.ts +107 -0
- package/src/functions/fhir.ts +247 -0
- package/src/functions/filtering.ts +120 -0
- package/src/functions/install.ts +19 -0
- package/src/functions/iteration.ts +21 -0
- package/src/functions/math.ts +167 -0
- package/src/functions/reflection.ts +24 -0
- package/src/functions/registry.ts +72 -0
- package/src/functions/sort.ts +68 -0
- package/src/functions/string.ts +413 -0
- package/src/functions/subsetting.ts +89 -0
- package/src/functions/tree.ts +112 -0
- package/src/functions/type-specifier.ts +29 -0
- package/src/functions/utility.ts +82 -0
- package/src/functions/variables.ts +36 -0
- package/src/index.ts +30 -0
- package/src/lexer/lexer.ts +341 -0
- package/src/lexer/tokens.ts +69 -0
- package/src/model/provider.ts +36 -0
- package/src/parser/ast.ts +164 -0
- package/src/parser/parser.ts +321 -0
- package/src/parser/precedence.ts +52 -0
- package/src/parser/printer.ts +114 -0
- package/src/r4/generated/resources-data.ts +28631 -0
- package/src/r4/generated/type-maps.ts +22145 -0
- package/src/r4/generated/types-data.ts +2208 -0
- package/src/r4/index.ts +107 -0
- package/src/r4/model-data.ts +17 -0
- package/src/sites/index.ts +285 -0
- package/src/typed/infer.ts +433 -0
- package/src/typed/perf-fixture.types.ts +143 -0
- package/src/values/collection.ts +57 -0
- package/src/values/datetime.ts +285 -0
- package/src/values/decimal.ts +213 -0
- package/src/values/numeric.ts +63 -0
- package/src/values/quantity.ts +358 -0
- package/src/values/temporal-arithmetic.ts +231 -0
- package/src/values/temporal-compare.ts +65 -0
- package/src/values/type-compat.ts +133 -0
- package/src/values/typed-value.ts +151 -0
- package/src/values/ucum.ts +181 -0
|
@@ -0,0 +1,1181 @@
|
|
|
1
|
+
import '../functions/install.ts'
|
|
2
|
+
|
|
3
|
+
import { BUILTIN_ENV_VARIABLE_NAMES } from '../engine/context.ts'
|
|
4
|
+
import { FhirPathSyntaxError, type SourceSpan } from '../errors.ts'
|
|
5
|
+
import { describeArity, functions } from '../functions/registry.ts'
|
|
6
|
+
import type { ElementInfo, ModelProvider } from '../model/provider.ts'
|
|
7
|
+
import type { AstNode } from '../parser/ast.ts'
|
|
8
|
+
import { parse } from '../parser/parser.ts'
|
|
9
|
+
import { unsatisfiedInput, type ValueKind, valueKindOfTypeName } from '../values/type-compat.ts'
|
|
10
|
+
import { FHIR_PRIMITIVE_TO_SYSTEM, typeLocalName } from '../values/typed-value.ts'
|
|
11
|
+
import { hasNestedUnboundedQuantifier } from './regex-safety.ts'
|
|
12
|
+
import {
|
|
13
|
+
type CustomFunctionSignature,
|
|
14
|
+
FUNCTION_SIGNATURES,
|
|
15
|
+
type FunctionSignature,
|
|
16
|
+
type InputSpec,
|
|
17
|
+
singleAnd,
|
|
18
|
+
unionStates,
|
|
19
|
+
withSingle,
|
|
20
|
+
} from './signatures.ts'
|
|
21
|
+
|
|
22
|
+
export interface AnalyzerDiagnostic {
|
|
23
|
+
severity: 'error' | 'warning'
|
|
24
|
+
/** Unresolved element, function, or variable name, when the diagnostic has one. */
|
|
25
|
+
name?: string
|
|
26
|
+
/** Stable rule identifier, e.g. `unknown-element` or `singleton-required`. */
|
|
27
|
+
code: string
|
|
28
|
+
message: string
|
|
29
|
+
span: SourceSpan
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A host variable known to the analyzer. Omit its types to keep the value unknown. */
|
|
33
|
+
export interface DeclaredVariable {
|
|
34
|
+
/** Candidate type names ('Patient', 'System.String'); omit to leave the type unknown. */
|
|
35
|
+
types?: string[]
|
|
36
|
+
/** True when the variable always holds at most one item. */
|
|
37
|
+
single?: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A host function declaration. Arity resolves the call; an optional signature checks it. */
|
|
41
|
+
export type SingleDeclaredFunction =
|
|
42
|
+
| { minArity?: number; maxArity?: number; expression?: never; signature?: CustomFunctionSignature }
|
|
43
|
+
| {
|
|
44
|
+
/** The body of an expression-defined CustomFunction; its presence pins the arity to 0. */
|
|
45
|
+
expression: unknown
|
|
46
|
+
minArity?: never
|
|
47
|
+
maxArity?: never
|
|
48
|
+
signature?: CustomFunctionSignature
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Same-name functions selected by the call focus. Unknown focus keeps only their shared claims. */
|
|
52
|
+
export interface OverloadedDeclaredFunction {
|
|
53
|
+
overloads: readonly SingleDeclaredFunction[]
|
|
54
|
+
expression?: never
|
|
55
|
+
minArity?: never
|
|
56
|
+
maxArity?: never
|
|
57
|
+
signature?: never
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type DeclaredFunction = SingleDeclaredFunction | OverloadedDeclaredFunction
|
|
61
|
+
|
|
62
|
+
export interface AnalyzeOptions {
|
|
63
|
+
model?: ModelProvider
|
|
64
|
+
/** Canonical type of the input the expression will run against, e.g. `FHIR.Patient` or `Patient`. */
|
|
65
|
+
inputType?: string
|
|
66
|
+
/** Host-supplied functions by name — EvaluateOptions.functions can be passed as-is. */
|
|
67
|
+
functions?: Record<string, DeclaredFunction>
|
|
68
|
+
/** Host-supplied environment variables by name (with or without the leading `%`). */
|
|
69
|
+
variables?: Record<string, DeclaredVariable>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Candidate type names and cardinality for one sub-expression. `undefined`
|
|
74
|
+
* means unknown and pauses checks that need that fact. `single` is true for at
|
|
75
|
+
* most one item, false for a possible collection, and undefined when unknown.
|
|
76
|
+
*/
|
|
77
|
+
interface StaticState {
|
|
78
|
+
types: string[] | undefined
|
|
79
|
+
single: boolean | undefined
|
|
80
|
+
/**
|
|
81
|
+
* Canonical resource types a Reference-valued state may point to (from
|
|
82
|
+
* `Reference.targetProfile`) — what resolve() yields. Carried by element
|
|
83
|
+
* navigation and cardinality-preserving functions; absent means unknown.
|
|
84
|
+
*/
|
|
85
|
+
targets?: string[]
|
|
86
|
+
/** True until the raw input has been narrowed or navigated through the model. */
|
|
87
|
+
rawInput?: boolean
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const UNKNOWN: StaticState = { types: undefined, single: undefined }
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Variables visible at one point in analysis. Operators and arguments receive a
|
|
94
|
+
* copy. A dynamic variable name prevents later unknown-variable diagnostics in
|
|
95
|
+
* that chain.
|
|
96
|
+
*/
|
|
97
|
+
interface VariableScope {
|
|
98
|
+
vars: Map<string, StaticState>
|
|
99
|
+
hasDynamic: boolean
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function emptyScope(): VariableScope {
|
|
103
|
+
return { vars: new Map(), hasDynamic: false }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function forkScope(scope: VariableScope): VariableScope {
|
|
107
|
+
return { vars: new Map(scope.vars), hasDynamic: scope.hasDynamic }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface AnalysisDetails {
|
|
111
|
+
diagnostics: AnalyzerDiagnostic[]
|
|
112
|
+
/**
|
|
113
|
+
* The expression's inferred result: canonical type names and cardinality, the
|
|
114
|
+
* same StaticState the checks run on. `types: undefined` means the analyzer
|
|
115
|
+
* cannot see through the expression (an unknown region), and `single`
|
|
116
|
+
* undefined means the cardinality is unknown — neither is an error. Lets a
|
|
117
|
+
* caller cross-check a declared type against what the expression really
|
|
118
|
+
* yields (see `analyzeDto`).
|
|
119
|
+
*/
|
|
120
|
+
result: { types: string[] | undefined; single: boolean | undefined }
|
|
121
|
+
/**
|
|
122
|
+
* `Type.element` paths the expression reads (local type names), deduped in
|
|
123
|
+
* first-visit order — HAPI's `elementDependencies`. Lets callers know which
|
|
124
|
+
* elements an expression depends on, e.g. for change tracking or editors.
|
|
125
|
+
*/
|
|
126
|
+
elementDependencies: string[]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Statically check one expression against the model: spec §11's strict-mode rules
|
|
131
|
+
* (singleton misuse, wrong operand and argument types, incomparable equality)
|
|
132
|
+
* plus unknown elements, functions, arities, type names, and variables.
|
|
133
|
+
* See: https://hl7.org/fhirpath/en/index.html#type-safety-and-strict-evaluation
|
|
134
|
+
*/
|
|
135
|
+
export function analyzeExpression(expression: string, options?: AnalyzeOptions): AnalyzerDiagnostic[] {
|
|
136
|
+
return analyzeExpressionDetailed(expression, options).diagnostics
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Analyzes one source site with only facts visible in that file. A declared root
|
|
141
|
+
* does not prove which variables the later call provides. DTO sites may also
|
|
142
|
+
* receive variables and functions from base classes, other modules, or
|
|
143
|
+
* `project()`. Such sites omit unknown-variable diagnostics and report an
|
|
144
|
+
* unknown function only when it resembles a local column. A DTO without a known
|
|
145
|
+
* root receives syntax checks only. Use `analyzeDto` when the class and engine
|
|
146
|
+
* are loaded.
|
|
147
|
+
*/
|
|
148
|
+
export function analyzeSite(
|
|
149
|
+
site: {
|
|
150
|
+
expression: string
|
|
151
|
+
inputType?: string
|
|
152
|
+
dto?: true
|
|
153
|
+
/** Functions the site's file declares — a DTO's `@column` fields (see `columnFunctionDeclaration`). */
|
|
154
|
+
functions?: Readonly<Record<string, DeclaredFunction>>
|
|
155
|
+
},
|
|
156
|
+
options?: AnalyzeOptions
|
|
157
|
+
): AnalyzerDiagnostic[] {
|
|
158
|
+
const declared = { ...site.functions, ...options?.functions }
|
|
159
|
+
const merged: AnalyzeOptions = {
|
|
160
|
+
...options,
|
|
161
|
+
...(site.inputType !== undefined && { inputType: site.inputType }),
|
|
162
|
+
...(Object.keys(declared).length > 0 && { functions: declared }),
|
|
163
|
+
}
|
|
164
|
+
const diagnostics = analyzeExpression(site.expression, merged)
|
|
165
|
+
if (site.dto !== true) {
|
|
166
|
+
return site.inputType === undefined
|
|
167
|
+
? diagnostics
|
|
168
|
+
: diagnostics.filter(diagnostic => diagnostic.code !== 'unknown-variable')
|
|
169
|
+
}
|
|
170
|
+
if (site.inputType === undefined) {
|
|
171
|
+
return diagnostics.filter(diagnostic => diagnostic.code === 'syntax')
|
|
172
|
+
}
|
|
173
|
+
const columns = Object.keys(site.functions ?? {})
|
|
174
|
+
return diagnostics.filter(diagnostic => {
|
|
175
|
+
if (diagnostic.code === 'unknown-variable') {
|
|
176
|
+
return false
|
|
177
|
+
}
|
|
178
|
+
if (diagnostic.code !== 'unknown-function') {
|
|
179
|
+
return true
|
|
180
|
+
}
|
|
181
|
+
return diagnostic.name !== undefined && nearestName(diagnostic.name, columns) !== undefined
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** `analyzeExpression` plus the element paths the expression touches and its inferred result type. */
|
|
186
|
+
export function analyzeExpressionDetailed(expression: string, options?: AnalyzeOptions): AnalysisDetails {
|
|
187
|
+
let ast: AstNode
|
|
188
|
+
try {
|
|
189
|
+
ast = parse(expression)
|
|
190
|
+
} catch (error) {
|
|
191
|
+
/* v8 ignore start -- parse only throws syntax errors */
|
|
192
|
+
if (!(error instanceof FhirPathSyntaxError)) {
|
|
193
|
+
throw error
|
|
194
|
+
}
|
|
195
|
+
/* v8 ignore stop */
|
|
196
|
+
return {
|
|
197
|
+
diagnostics: [{ severity: 'error', code: 'syntax', message: error.message, span: error.span }],
|
|
198
|
+
elementDependencies: [],
|
|
199
|
+
result: { types: undefined, single: undefined },
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const analyzer = new Analyzer(options)
|
|
203
|
+
const state = analyzer.walk(ast, analyzer.rootState(), emptyScope())
|
|
204
|
+
return {
|
|
205
|
+
diagnostics: analyzer.diagnostics,
|
|
206
|
+
elementDependencies: [...analyzer.dependencies],
|
|
207
|
+
result: { types: state.types, single: state.single },
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
class Analyzer {
|
|
212
|
+
readonly diagnostics: AnalyzerDiagnostic[] = []
|
|
213
|
+
readonly dependencies = new Set<string>()
|
|
214
|
+
private readonly model: ModelProvider | undefined
|
|
215
|
+
private readonly inputType: string | undefined
|
|
216
|
+
private readonly frames: StaticState[] = []
|
|
217
|
+
/** Every declaration of each host-supplied name; one entry unless the name is overloaded. */
|
|
218
|
+
private readonly customFunctions: ReadonlyMap<string, readonly ResolvedDeclaration[]>
|
|
219
|
+
private readonly declaredVariables: ReadonlyMap<string, DeclaredVariable>
|
|
220
|
+
|
|
221
|
+
constructor(options: AnalyzeOptions | undefined) {
|
|
222
|
+
this.model = options?.model
|
|
223
|
+
const inputType = options?.inputType
|
|
224
|
+
this.inputType = inputType === undefined ? undefined : (this.model?.resolveType(inputType) ?? inputType)
|
|
225
|
+
this.customFunctions = new Map(
|
|
226
|
+
Object.entries(options?.functions ?? {}).map(([name, declared]) => [
|
|
227
|
+
name,
|
|
228
|
+
('overloads' in declared ? declared.overloads : [declared]).map(resolvedDeclaration),
|
|
229
|
+
])
|
|
230
|
+
)
|
|
231
|
+
this.declaredVariables = new Map(
|
|
232
|
+
Object.entries(options?.variables ?? {}).map(([name, variable]) => [
|
|
233
|
+
name.startsWith('%') ? name.slice(1) : name,
|
|
234
|
+
variable,
|
|
235
|
+
])
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
rootState(): StaticState {
|
|
240
|
+
return this.inputType === undefined ? UNKNOWN : { types: [this.inputType], single: true, rawInput: true }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
walk(node: AstNode, input: StaticState, scope: VariableScope): StaticState {
|
|
244
|
+
switch (node.kind) {
|
|
245
|
+
case 'null':
|
|
246
|
+
// The empty literal `{}` is statically at most one item, so it satisfies
|
|
247
|
+
// singleton operands (`{} + 1`, `{} and true` are spec-legal).
|
|
248
|
+
return { types: [], single: true }
|
|
249
|
+
case 'boolean':
|
|
250
|
+
return { types: ['System.Boolean'], single: true }
|
|
251
|
+
case 'string':
|
|
252
|
+
return { types: ['System.String'], single: true }
|
|
253
|
+
case 'number':
|
|
254
|
+
return {
|
|
255
|
+
types: [node.isLong ? 'System.Long' : node.isDecimal ? 'System.Decimal' : 'System.Integer'],
|
|
256
|
+
single: true,
|
|
257
|
+
}
|
|
258
|
+
case 'date':
|
|
259
|
+
return { types: ['System.Date'], single: true }
|
|
260
|
+
case 'dateTime':
|
|
261
|
+
return { types: ['System.DateTime'], single: true }
|
|
262
|
+
case 'time':
|
|
263
|
+
return { types: ['System.Time'], single: true }
|
|
264
|
+
case 'quantity':
|
|
265
|
+
return { types: ['System.Quantity'], single: true }
|
|
266
|
+
case 'external':
|
|
267
|
+
return this.walkExternal(node, scope)
|
|
268
|
+
case 'special':
|
|
269
|
+
return this.walkSpecial(node.name)
|
|
270
|
+
case 'identifier':
|
|
271
|
+
return this.walkIdentifier(node, input)
|
|
272
|
+
case 'dot':
|
|
273
|
+
// One scope threads the whole chain, so defineVariable() in an earlier
|
|
274
|
+
// link is visible to later links — exactly like the runtime.
|
|
275
|
+
return this.walk(node.right, this.walk(node.left, input, scope), scope)
|
|
276
|
+
case 'indexer': {
|
|
277
|
+
const target = this.walk(node.target, input, scope)
|
|
278
|
+
const index = this.walk(node.index, input, scope)
|
|
279
|
+
this.requireKind(index, 'Numeric', node.index.span, 'the indexer expects a single Integer')
|
|
280
|
+
return withSingle(target, true)
|
|
281
|
+
}
|
|
282
|
+
case 'call':
|
|
283
|
+
return this.walkCall(node, input, scope)
|
|
284
|
+
case 'unary': {
|
|
285
|
+
const operand = this.walk(node.operand, input, scope)
|
|
286
|
+
this.requireKind(operand, 'Numeric', node.operand.span, `unary '${node.operator}' expects a single number`)
|
|
287
|
+
return operand
|
|
288
|
+
}
|
|
289
|
+
case 'binary':
|
|
290
|
+
return this.walkBinary(node, input, scope)
|
|
291
|
+
case 'typeOp':
|
|
292
|
+
return this.walkTypeOp(node, input, scope)
|
|
293
|
+
/* v8 ignore start -- exhaustive fallback */
|
|
294
|
+
default: {
|
|
295
|
+
const unreachable: never = node
|
|
296
|
+
throw new Error(`Unhandled node ${String(unreachable)}`)
|
|
297
|
+
}
|
|
298
|
+
/* v8 ignore stop */
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* `%name`: defineVariable() bindings and built-in variables resolve with their
|
|
304
|
+
* known state; anything else is an undefined variable (spec §9), the same
|
|
305
|
+
* check the runtime applies. Host-supplied variables must be declared to the
|
|
306
|
+
* analyzer (AnalyzeOptions is the place this will grow).
|
|
307
|
+
*/
|
|
308
|
+
private walkExternal(node: AstNode & { kind: 'external' }, scope: VariableScope): StaticState {
|
|
309
|
+
const defined = scope.vars.get(node.name)
|
|
310
|
+
if (defined !== undefined) {
|
|
311
|
+
return defined
|
|
312
|
+
}
|
|
313
|
+
const declared = this.declaredVariables.get(node.name)
|
|
314
|
+
if (declared !== undefined) {
|
|
315
|
+
return { types: declared.types?.map(type => this.canonicalize(type)), single: declared.single }
|
|
316
|
+
}
|
|
317
|
+
switch (node.name) {
|
|
318
|
+
case 'context':
|
|
319
|
+
case 'resource':
|
|
320
|
+
case 'rootResource':
|
|
321
|
+
// Contained-resource re-rooting is a later refinement, mirroring the runtime.
|
|
322
|
+
return this.rootState()
|
|
323
|
+
case 'ucum':
|
|
324
|
+
case 'sct':
|
|
325
|
+
case 'loinc':
|
|
326
|
+
return { types: ['System.String'], single: true }
|
|
327
|
+
default:
|
|
328
|
+
break
|
|
329
|
+
}
|
|
330
|
+
// FHIR-defined families expand to HL7 urls: %`vs-[name]`, %`ext-[name]`.
|
|
331
|
+
if (node.name.startsWith('vs-') || node.name.startsWith('ext-')) {
|
|
332
|
+
return { types: ['System.String'], single: true }
|
|
333
|
+
}
|
|
334
|
+
// A dynamically-named defineVariable() earlier in the chain may have bound
|
|
335
|
+
// this name, so reporting it as undefined could be wrong — stay quiet.
|
|
336
|
+
if (!scope.hasDynamic) {
|
|
337
|
+
this.report('unknown-variable', `Undefined environment variable %${node.name}`, node.span, 'error', node.name)
|
|
338
|
+
}
|
|
339
|
+
return UNKNOWN
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private walkSpecial(name: 'this' | 'index' | 'total'): StaticState {
|
|
343
|
+
if (name === 'this') {
|
|
344
|
+
return this.frames.at(-1) ?? this.rootState()
|
|
345
|
+
}
|
|
346
|
+
return name === 'index' ? { types: ['System.Integer'], single: true } : UNKNOWN
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* A quantity's components (spec §4: `value` and `unit`), the one System type
|
|
351
|
+
* with navigable elements. The runtime reads them off the quantity's raw
|
|
352
|
+
* `{ value, unit }` shape (a `toQuantity()` result, a quantity literal), so
|
|
353
|
+
* the analyzer must know them too or flag working navigation.
|
|
354
|
+
*/
|
|
355
|
+
private static readonly SYSTEM_QUANTITY_ELEMENTS: ReadonlyMap<string, ElementInfo> = new Map([
|
|
356
|
+
['value', { types: ['System.Decimal'], isCollection: false, isChoice: false }],
|
|
357
|
+
['unit', { types: ['System.String'], isCollection: false, isChoice: false }],
|
|
358
|
+
])
|
|
359
|
+
|
|
360
|
+
private walkIdentifier(node: AstNode & { kind: 'identifier' }, input: StaticState): StaticState {
|
|
361
|
+
// Navigating from a statically empty input yields empty — nothing to check.
|
|
362
|
+
if (input.types !== undefined && input.types.length === 0) {
|
|
363
|
+
return { types: [], single: true }
|
|
364
|
+
}
|
|
365
|
+
if (input.types === undefined) {
|
|
366
|
+
// Even with an unknown input, a root identifier naming a model type anchors
|
|
367
|
+
// the state — this is what checks `Patient.nope` without an inputType option.
|
|
368
|
+
const asType = this.model?.resolveType(node.name)
|
|
369
|
+
if (asType !== undefined) {
|
|
370
|
+
return { types: [asType], single: true }
|
|
371
|
+
}
|
|
372
|
+
return UNKNOWN
|
|
373
|
+
}
|
|
374
|
+
// Root rule: an identifier naming the (super)type of the context is the context.
|
|
375
|
+
if (this.model) {
|
|
376
|
+
const asType = this.model.resolveType(node.name)
|
|
377
|
+
if (asType !== undefined && input.types.some(type => this.model?.isSubtypeOf(type, asType))) {
|
|
378
|
+
// The runtime matches a type name against the raw input only through the
|
|
379
|
+
// resourceType discriminator (values/typed-value.ts), so a non-resource
|
|
380
|
+
// name never matches there and the whole path navigates to empty.
|
|
381
|
+
if (input.rawInput === true && !this.isResourceType(asType)) {
|
|
382
|
+
this.report(
|
|
383
|
+
'datatype-root',
|
|
384
|
+
`'${node.name}' is not a resource type, and a type-name root matches only a resource's resourceType, so this always evaluates to empty — navigate from the input with a relative path`,
|
|
385
|
+
node.span
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
return input
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const found: string[] = []
|
|
392
|
+
let isCollection = false
|
|
393
|
+
// Reference targets (Reference.targetProfile) accumulate so a later
|
|
394
|
+
// resolve() yields their union; one unconstrained reference makes the
|
|
395
|
+
// whole set unknown.
|
|
396
|
+
let targets: string[] | undefined = []
|
|
397
|
+
for (const type of input.types) {
|
|
398
|
+
const element =
|
|
399
|
+
type === 'System.Quantity'
|
|
400
|
+
? Analyzer.SYSTEM_QUANTITY_ELEMENTS.get(node.name)
|
|
401
|
+
: this.model?.getElement(type, node.name)
|
|
402
|
+
if (element) {
|
|
403
|
+
// elementDependencies names model elements; System.Quantity's components
|
|
404
|
+
// are not ones (there is no FHIR `Quantity.value` dependency here).
|
|
405
|
+
if (!type.startsWith('System.')) {
|
|
406
|
+
this.dependencies.add(`${typeLocalName(type)}.${node.name}`)
|
|
407
|
+
}
|
|
408
|
+
isCollection = isCollection || element.isCollection
|
|
409
|
+
for (const elementType of element.types) {
|
|
410
|
+
found.push(this.canonicalize(elementType))
|
|
411
|
+
}
|
|
412
|
+
if (element.types.some(elementType => typeLocalName(elementType) === 'Reference')) {
|
|
413
|
+
targets = this.mergeTargets(targets, element)
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (found.length === 0) {
|
|
418
|
+
if (this.model) {
|
|
419
|
+
const hint = this.isChoiceKeyMisuse(input.types, node.name)
|
|
420
|
+
? '; choice elements use their stem name'
|
|
421
|
+
: didYouMean(node.name, this.elementNames(input.types))
|
|
422
|
+
this.report(
|
|
423
|
+
'unknown-element',
|
|
424
|
+
`Element '${node.name}' is not defined on ${input.types.join(' | ')}${hint}`,
|
|
425
|
+
node.span,
|
|
426
|
+
'error',
|
|
427
|
+
node.name
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
return UNKNOWN
|
|
431
|
+
}
|
|
432
|
+
const state: StaticState = { types: [...new Set(found)], single: singleAnd(input.single, !isCollection) }
|
|
433
|
+
if (targets !== undefined && targets.length > 0) {
|
|
434
|
+
state.targets = [...new Set(targets)]
|
|
435
|
+
}
|
|
436
|
+
return state
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* True when `canonical` is the model's Resource base or derives from it —
|
|
441
|
+
* the types whose instances carry a resourceType discriminator. A model
|
|
442
|
+
* without a Resource base cannot make the distinction, so the check stays
|
|
443
|
+
* permissive there.
|
|
444
|
+
*/
|
|
445
|
+
private isResourceType(canonical: string): boolean {
|
|
446
|
+
const base = this.model?.resolveType('Resource')
|
|
447
|
+
return base === undefined || this.model?.isSubtypeOf(canonical, base) === true
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private canonicalize(elementType: string): string {
|
|
451
|
+
if (elementType.startsWith('System.')) {
|
|
452
|
+
return elementType
|
|
453
|
+
}
|
|
454
|
+
return this.model?.resolveType(elementType) ?? elementType
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* A Reference-typed element's targets folded into the accumulated set.
|
|
459
|
+
* One unconstrained reference makes the whole set unknown, and unknown
|
|
460
|
+
* stays unknown; otherwise the result is the canonicalized union.
|
|
461
|
+
*/
|
|
462
|
+
private mergeTargets(current: string[] | undefined, element: ElementInfo): string[] | undefined {
|
|
463
|
+
if (current === undefined || element.referenceTargets === undefined) {
|
|
464
|
+
return undefined
|
|
465
|
+
}
|
|
466
|
+
return [...current, ...element.referenceTargets.map(target => this.canonicalize(target))]
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* True for `valueQuantity`-style keys whose stem is a choice element on one of the
|
|
471
|
+
* input types — the same test the evaluator uses (engine/navigation.ts), so the
|
|
472
|
+
* static diagnostic and the runtime error carry the same guidance.
|
|
473
|
+
*/
|
|
474
|
+
private isChoiceKeyMisuse(types: string[], name: string): boolean {
|
|
475
|
+
if (!this.model) {
|
|
476
|
+
return false
|
|
477
|
+
}
|
|
478
|
+
for (const type of types) {
|
|
479
|
+
for (let position = 1; position < name.length; position++) {
|
|
480
|
+
if (!/[A-Z]/.test(name[position] as string)) {
|
|
481
|
+
continue
|
|
482
|
+
}
|
|
483
|
+
const stem = name.slice(0, position)
|
|
484
|
+
if (this.model.getElement(type, stem)?.isChoice === true) {
|
|
485
|
+
return true
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return false
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Every element name the input types can offer, for typo suggestions. */
|
|
493
|
+
private elementNames(types: string[]): string[] {
|
|
494
|
+
if (this.model?.listElements === undefined) {
|
|
495
|
+
return []
|
|
496
|
+
}
|
|
497
|
+
const names: string[] = []
|
|
498
|
+
for (const type of types) {
|
|
499
|
+
const elements = this.model.listElements(type)
|
|
500
|
+
if (elements !== undefined) {
|
|
501
|
+
names.push(...elements)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return names
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* A call: resolve the function (built-ins first — EvaluateOptions.functions
|
|
509
|
+
* cannot override them either), then hand each concern to its own step:
|
|
510
|
+
* arity, input shape, arguments, per-function effects, result.
|
|
511
|
+
*/
|
|
512
|
+
private walkCall(node: AstNode & { kind: 'call' }, input: StaticState, scope: VariableScope): StaticState {
|
|
513
|
+
const registered = functions.get(node.name)
|
|
514
|
+
const custom = registered === undefined ? this.declarationFor(node.name, input) : undefined
|
|
515
|
+
const resolved = registered ?? custom
|
|
516
|
+
if (resolved === undefined) {
|
|
517
|
+
this.report(
|
|
518
|
+
'unknown-function',
|
|
519
|
+
`Unrecognized function '${node.name}'${didYouMean(node.name, [...functions.keys(), ...this.customFunctions.keys()])}`,
|
|
520
|
+
node.span,
|
|
521
|
+
'error',
|
|
522
|
+
node.name
|
|
523
|
+
)
|
|
524
|
+
this.walkUncheckedArguments(node, input, scope)
|
|
525
|
+
return UNKNOWN
|
|
526
|
+
}
|
|
527
|
+
this.checkArity(node, resolved)
|
|
528
|
+
const signature = registered !== undefined ? FUNCTION_SIGNATURES[node.name] : this.toSignature(custom?.signature)
|
|
529
|
+
if (!signature) {
|
|
530
|
+
this.walkUncheckedArguments(node, input, scope)
|
|
531
|
+
return UNKNOWN
|
|
532
|
+
}
|
|
533
|
+
this.checkCallInput(node, signature, input)
|
|
534
|
+
const { argStates, typeTarget } = this.walkArguments(node, signature, input, scope)
|
|
535
|
+
if (node.name === 'defineVariable') {
|
|
536
|
+
this.registerVariable(node, input, argStates, scope)
|
|
537
|
+
}
|
|
538
|
+
this.checkRegexPattern(node)
|
|
539
|
+
// ofType(X) filters and as(X) casts: both narrow to the named type,
|
|
540
|
+
// intersected with the known candidates.
|
|
541
|
+
if ((node.name === 'ofType' || node.name === 'as') && typeTarget !== undefined) {
|
|
542
|
+
return { types: this.narrowTypes(input, typeTarget, node.span), single: input.single }
|
|
543
|
+
}
|
|
544
|
+
return signature.result(input, argStates)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Selects a host declaration from the focus. One match keeps its full
|
|
549
|
+
* signature. Several matches are merged conservatively. No match merges their
|
|
550
|
+
* input types so `checkCallInput` can report one error.
|
|
551
|
+
*/
|
|
552
|
+
private declarationFor(name: string, input: StaticState): ResolvedDeclaration | undefined {
|
|
553
|
+
const candidates = this.customFunctions.get(name)
|
|
554
|
+
if (candidates === undefined || candidates.length <= 1) {
|
|
555
|
+
return candidates?.[0]
|
|
556
|
+
}
|
|
557
|
+
const focus = input.types ?? []
|
|
558
|
+
const fitting = candidates.filter(
|
|
559
|
+
candidate => unsatisfiedInput(this.model, candidate.signature?.input?.types, focus) === undefined
|
|
560
|
+
)
|
|
561
|
+
return fitting.length === 1 ? fitting[0] : mergedDeclaration(fitting.length === 0 ? candidates : fitting)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** Without a signature the arguments still walk (for their own diagnostics), each in a scope fork. */
|
|
565
|
+
private walkUncheckedArguments(node: AstNode & { kind: 'call' }, input: StaticState, scope: VariableScope): void {
|
|
566
|
+
for (const argument of node.args) {
|
|
567
|
+
this.walk(argument, input, forkScope(scope))
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
private checkArity(node: AstNode & { kind: 'call' }, arity: { minArity?: number; maxArity?: number }): void {
|
|
572
|
+
const minArity = arity.minArity ?? 0
|
|
573
|
+
const maxArity = arity.maxArity ?? Number.POSITIVE_INFINITY
|
|
574
|
+
if (node.args.length < minArity || node.args.length > maxArity) {
|
|
575
|
+
this.report(
|
|
576
|
+
'wrong-arity',
|
|
577
|
+
`Function '${node.name}' expects ${describeArity(minArity, maxArity)}, got ${node.args.length}`,
|
|
578
|
+
node.span
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** The signature's input constraints: cardinality, value kind, and declared types. */
|
|
584
|
+
private checkCallInput(node: AstNode & { kind: 'call' }, signature: FunctionSignature, input: StaticState): void {
|
|
585
|
+
if (!signature.input) {
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
// A function written for one type (a DTO's `@column`), called on a focus
|
|
589
|
+
// that can never be that type. `unsatisfiedInput` holds the same rule the
|
|
590
|
+
// engine applies, so the two halves agree on what counts as a mistake.
|
|
591
|
+
const unsatisfied =
|
|
592
|
+
input.types === undefined ? undefined : unsatisfiedInput(this.model, signature.input.types, input.types)
|
|
593
|
+
if (unsatisfied !== undefined) {
|
|
594
|
+
this.report(
|
|
595
|
+
'input-type',
|
|
596
|
+
`${node.name}() expects ${unsatisfied.wanted.join(' | ')} as input, found ${unsatisfied.found.join(' | ')}`,
|
|
597
|
+
node.span
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
if (signature.input.singleton && input.types !== undefined && input.single === false) {
|
|
601
|
+
this.report(
|
|
602
|
+
'singleton-required',
|
|
603
|
+
`${node.name}() expects a single item as input, but this is a collection (spec §11)${NARROW_HINT}`,
|
|
604
|
+
node.span
|
|
605
|
+
)
|
|
606
|
+
}
|
|
607
|
+
if (signature.input.kind) {
|
|
608
|
+
this.requireKind(
|
|
609
|
+
{ types: input.types, single: true },
|
|
610
|
+
signature.input.kind,
|
|
611
|
+
node.span,
|
|
612
|
+
`${node.name}() expects a ${signature.input.kind} input`
|
|
613
|
+
)
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Walk every argument per its spec — lambdas against a $this frame,
|
|
619
|
+
* type names against the model, values against $this — collecting the
|
|
620
|
+
* analyzed state per position (undefined for type-name positions). Each
|
|
621
|
+
* argument gets its own scope copy, like the runtime's per-argument fork.
|
|
622
|
+
*/
|
|
623
|
+
private walkArguments(
|
|
624
|
+
node: AstNode & { kind: 'call' },
|
|
625
|
+
signature: FunctionSignature,
|
|
626
|
+
input: StaticState,
|
|
627
|
+
scope: VariableScope
|
|
628
|
+
): { argStates: (StaticState | undefined)[]; typeTarget: string | undefined } {
|
|
629
|
+
const argStates: (StaticState | undefined)[] = []
|
|
630
|
+
let typeTarget: string | undefined
|
|
631
|
+
node.args.forEach((argument, index) => {
|
|
632
|
+
const spec = signature.args?.[index] ?? signature.args?.at(-1)
|
|
633
|
+
if (spec === 'expression' || spec === 'condition' || spec === 'sort-key') {
|
|
634
|
+
// A top-level unary '-' on a sort key marks descending order (any type),
|
|
635
|
+
// mirroring how sort() reads the AST; only the key itself is analyzed.
|
|
636
|
+
const body =
|
|
637
|
+
spec === 'sort-key' && argument.kind === 'unary' && argument.operator === '-' ? argument.operand : argument
|
|
638
|
+
// $this is one item of the input — same candidates and reference targets.
|
|
639
|
+
const itemState = withSingle(input, true)
|
|
640
|
+
this.frames.push(itemState)
|
|
641
|
+
const state = this.walk(body, itemState, forkScope(scope))
|
|
642
|
+
this.frames.pop()
|
|
643
|
+
argStates.push(state)
|
|
644
|
+
if (spec === 'condition') {
|
|
645
|
+
this.requireSingle(state, argument.span, `${node.name}() expects a single Boolean criterion`)
|
|
646
|
+
if (!isCollection(state)) {
|
|
647
|
+
this.requireKind(state, 'Boolean', argument.span, `${node.name}() expects a Boolean criterion`)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return
|
|
651
|
+
}
|
|
652
|
+
if (spec === 'type-name') {
|
|
653
|
+
argStates.push(undefined)
|
|
654
|
+
const resolved = this.checkTypeArgument(node.name, argument)
|
|
655
|
+
if (index === 0) {
|
|
656
|
+
typeTarget = resolved
|
|
657
|
+
}
|
|
658
|
+
return
|
|
659
|
+
}
|
|
660
|
+
// Value arguments evaluate against $this, mirroring the runtime.
|
|
661
|
+
const argState = this.walk(argument, this.frames.at(-1) ?? this.rootState(), forkScope(scope))
|
|
662
|
+
argStates.push(argState)
|
|
663
|
+
if (spec !== undefined && spec !== 'any') {
|
|
664
|
+
if (argState.types !== undefined && argState.single === false) {
|
|
665
|
+
this.report(
|
|
666
|
+
'argument-singleton',
|
|
667
|
+
`${node.name}() expects a single ${spec} argument, but this is a collection (spec §11)${NARROW_HINT}`,
|
|
668
|
+
argument.span,
|
|
669
|
+
'warning'
|
|
670
|
+
)
|
|
671
|
+
} else {
|
|
672
|
+
this.requireKind(argState, spec, argument.span, `${node.name}() expects a ${spec} argument`)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
})
|
|
676
|
+
return { argStates, typeTarget }
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* The matches() family compiles its pattern with the backtracking JS RegExp,
|
|
681
|
+
* which cannot be timed out — flag exponential-shaped literal patterns.
|
|
682
|
+
*/
|
|
683
|
+
private checkRegexPattern(node: AstNode & { kind: 'call' }): void {
|
|
684
|
+
if (!REGEX_PATTERN_FUNCTIONS.has(node.name)) {
|
|
685
|
+
return
|
|
686
|
+
}
|
|
687
|
+
const pattern = node.args[0]
|
|
688
|
+
if (pattern?.kind === 'string' && hasNestedUnboundedQuantifier(pattern.value)) {
|
|
689
|
+
this.report(
|
|
690
|
+
'regex-backtracking',
|
|
691
|
+
`The regular expression nests unbounded repetition, which can backtrack catastrophically on non-matching input (ReDoS); rewrite it or supply a linear-time engine via EvaluateOptions.regex`,
|
|
692
|
+
pattern.span,
|
|
693
|
+
'warning'
|
|
694
|
+
)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Track a defineVariable() binding in the chain's scope, with the same two
|
|
700
|
+
* rules the runtime enforces: no overriding environment variables, no
|
|
701
|
+
* redefining a name already in scope. Dynamic names cannot be tracked.
|
|
702
|
+
*/
|
|
703
|
+
private registerVariable(
|
|
704
|
+
node: AstNode & { kind: 'call' },
|
|
705
|
+
input: StaticState,
|
|
706
|
+
argStates: (StaticState | undefined)[],
|
|
707
|
+
scope: VariableScope
|
|
708
|
+
): void {
|
|
709
|
+
const nameNode = node.args[0]
|
|
710
|
+
if (nameNode === undefined || nameNode.kind !== 'string') {
|
|
711
|
+
scope.hasDynamic = true
|
|
712
|
+
return
|
|
713
|
+
}
|
|
714
|
+
const name = nameNode.value
|
|
715
|
+
if (BUILTIN_ENV_VARIABLE_NAMES.has(name) || this.declaredVariables.has(name)) {
|
|
716
|
+
this.report('variable-override', `Cannot override the environment variable %${name}`, nameNode.span)
|
|
717
|
+
return
|
|
718
|
+
}
|
|
719
|
+
if (scope.vars.has(name)) {
|
|
720
|
+
this.report('variable-redefined', `Variable %${name} is already defined in this scope`, nameNode.span)
|
|
721
|
+
return
|
|
722
|
+
}
|
|
723
|
+
// Without a value expression the variable holds the function's input.
|
|
724
|
+
scope.vars.set(name, argStates[1] ?? input)
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Converts a host function's declared signature into the analyzer's internal
|
|
729
|
+
* shape. Result type names canonicalize once here, and a missing result stays
|
|
730
|
+
* unknown. The input passes through as declared, because `unsatisfiedInput`
|
|
731
|
+
* canonicalizes the names it compares, so a local name such as
|
|
732
|
+
* 'CodeableConcept' works without a second pass.
|
|
733
|
+
*/
|
|
734
|
+
private toSignature(declared: CustomFunctionSignature | undefined): FunctionSignature | undefined {
|
|
735
|
+
if (declared === undefined) {
|
|
736
|
+
return undefined
|
|
737
|
+
}
|
|
738
|
+
const types = declared.result?.types?.map(type => this.canonicalize(type))
|
|
739
|
+
const single = declared.result?.single
|
|
740
|
+
return {
|
|
741
|
+
...(declared.input !== undefined && { input: declared.input }),
|
|
742
|
+
...(declared.args !== undefined && { args: declared.args }),
|
|
743
|
+
result: () => ({ types, single }),
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* The candidate types that survive narrowing to `target`: subtypes of the
|
|
749
|
+
* target pass through unchanged, supertypes narrow to the target itself.
|
|
750
|
+
* Warns when no candidate can ever match — the result is provably empty.
|
|
751
|
+
*/
|
|
752
|
+
private narrowTypes(input: StaticState, target: string, span: SourceSpan): string[] | undefined {
|
|
753
|
+
if (input.types === undefined) {
|
|
754
|
+
return [target]
|
|
755
|
+
}
|
|
756
|
+
const survivors = new Set<string>()
|
|
757
|
+
for (const type of input.types) {
|
|
758
|
+
if (this.isTypeCompatible(type, target)) {
|
|
759
|
+
survivors.add(type)
|
|
760
|
+
} else if (this.isTypeCompatible(target, type)) {
|
|
761
|
+
survivors.add(target)
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (survivors.size === 0 && input.types.length > 0) {
|
|
765
|
+
this.report(
|
|
766
|
+
'always-empty',
|
|
767
|
+
`No candidate type (${input.types.join(' | ')}) can be a ${target}, so this is always empty`,
|
|
768
|
+
span,
|
|
769
|
+
'warning'
|
|
770
|
+
)
|
|
771
|
+
}
|
|
772
|
+
return [...survivors]
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/** True when every `type` value is a `base` value, including FHIR-primitive → System subtyping. */
|
|
776
|
+
private isTypeCompatible(type: string, base: string): boolean {
|
|
777
|
+
if (type === base || this.model?.isSubtypeOf(type, base) === true) {
|
|
778
|
+
return true
|
|
779
|
+
}
|
|
780
|
+
return base.startsWith('System.') && FHIR_PRIMITIVE_TO_SYSTEM[typeLocalName(type)] === base
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** Check a type-name argument; returns its canonical name, or undefined when unknown. */
|
|
784
|
+
private checkTypeArgument(functionName: string, argument: AstNode): string | undefined {
|
|
785
|
+
const parts = typeSpecifierParts(argument)
|
|
786
|
+
if (parts === undefined) {
|
|
787
|
+
this.report('unknown-type', `${functionName}() expects a type name argument`, argument.span)
|
|
788
|
+
return undefined
|
|
789
|
+
}
|
|
790
|
+
return this.checkTypeName(parts, argument.span)
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/** Check a type name; returns its canonical form, or undefined when it was reported unknown. */
|
|
794
|
+
private checkTypeName(parts: string[], span: SourceSpan): string | undefined {
|
|
795
|
+
if (parts.length === 2 && parts[0] === 'System') {
|
|
796
|
+
if (!SYSTEM_TYPE_NAMES.has(parts[1] as string)) {
|
|
797
|
+
this.report('unknown-type', `Unknown type 'System.${parts[1]}'`, span)
|
|
798
|
+
return undefined
|
|
799
|
+
}
|
|
800
|
+
return `System.${parts[1]}`
|
|
801
|
+
}
|
|
802
|
+
const name = parts.length === 2 ? (parts[1] as string) : (parts[0] as string)
|
|
803
|
+
if (parts.length === 2 && this.model && parts[0] !== this.model.namespace) {
|
|
804
|
+
this.report('unknown-type', `Unknown namespace '${parts[0]}'`, span)
|
|
805
|
+
return undefined
|
|
806
|
+
}
|
|
807
|
+
const resolved = this.model?.resolveType(name)
|
|
808
|
+
if (this.model && resolved === undefined && !SYSTEM_TYPE_NAMES.has(name)) {
|
|
809
|
+
this.report('unknown-type', `Unknown type '${parts.join('.')}'`, span)
|
|
810
|
+
return undefined
|
|
811
|
+
}
|
|
812
|
+
return resolved ?? (SYSTEM_TYPE_NAMES.has(name) ? `System.${name}` : name)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
private walkBinary(node: AstNode & { kind: 'binary' }, input: StaticState, scope: VariableScope): StaticState {
|
|
816
|
+
// Operator operands are separate chains: each analyzes against its own scope
|
|
817
|
+
// copy, so defineVariable() in one side is invisible to the other, exactly
|
|
818
|
+
// like the runtime.
|
|
819
|
+
const left = this.walk(node.left, input, forkScope(scope))
|
|
820
|
+
const right = this.walk(node.right, input, forkScope(scope))
|
|
821
|
+
switch (node.operator) {
|
|
822
|
+
case '+':
|
|
823
|
+
case '-':
|
|
824
|
+
case '*':
|
|
825
|
+
case '/':
|
|
826
|
+
case 'div':
|
|
827
|
+
case 'mod': {
|
|
828
|
+
this.checkArithmetic(node.operator, left, right, node.span)
|
|
829
|
+
// Quantity arithmetic yields Quantity (4.0 'g' / 2.0 'm' is 2 'g/m');
|
|
830
|
+
// plain division yields Decimal; everything else keeps the operand type.
|
|
831
|
+
const quantity =
|
|
832
|
+
(node.operator === '*' || node.operator === '/') &&
|
|
833
|
+
(kindOf(left) === 'Quantity' || kindOf(right) === 'Quantity')
|
|
834
|
+
const types = quantity
|
|
835
|
+
? ['System.Quantity']
|
|
836
|
+
: node.operator === '/'
|
|
837
|
+
? ['System.Decimal']
|
|
838
|
+
: (left.types ?? right.types)
|
|
839
|
+
return { types, single: true }
|
|
840
|
+
}
|
|
841
|
+
case '&':
|
|
842
|
+
this.requireSingle(left, node.left.span, "'&' expects single-item operands")
|
|
843
|
+
this.requireSingle(right, node.right.span, "'&' expects single-item operands")
|
|
844
|
+
this.requireKind(left, 'String', node.left.span, "'&' expects String operands")
|
|
845
|
+
this.requireKind(right, 'String', node.right.span, "'&' expects String operands")
|
|
846
|
+
return { types: ['System.String'], single: true }
|
|
847
|
+
case '<':
|
|
848
|
+
case '>':
|
|
849
|
+
case '<=':
|
|
850
|
+
case '>=':
|
|
851
|
+
this.requireSingle(left, node.left.span, `Operator '${node.operator}' expects single-item operands`)
|
|
852
|
+
this.requireSingle(right, node.right.span, `Operator '${node.operator}' expects single-item operands`)
|
|
853
|
+
this.checkComparable(left, right, node.span)
|
|
854
|
+
return { types: ['System.Boolean'], single: true }
|
|
855
|
+
case '=':
|
|
856
|
+
case '!=':
|
|
857
|
+
case '~':
|
|
858
|
+
case '!~':
|
|
859
|
+
this.checkEquality(left, right, node.span)
|
|
860
|
+
return { types: ['System.Boolean'], single: true }
|
|
861
|
+
case 'and':
|
|
862
|
+
case 'or':
|
|
863
|
+
case 'xor':
|
|
864
|
+
case 'implies': {
|
|
865
|
+
// Any single item satisfies a Boolean operand through the implicit-exists
|
|
866
|
+
// rule, so only cardinality is checkable here.
|
|
867
|
+
this.requireSingle(left, node.left.span, `'${node.operator}' expects single-item operands`)
|
|
868
|
+
this.requireSingle(right, node.right.span, `'${node.operator}' expects single-item operands`)
|
|
869
|
+
return { types: ['System.Boolean'], single: true }
|
|
870
|
+
}
|
|
871
|
+
case '|': {
|
|
872
|
+
// A statically empty side contributes nothing: `{} | true` is one item.
|
|
873
|
+
if (left.types?.length === 0) {
|
|
874
|
+
return right
|
|
875
|
+
}
|
|
876
|
+
if (right.types?.length === 0) {
|
|
877
|
+
return left
|
|
878
|
+
}
|
|
879
|
+
return withSingle(unionStates([left, right]), false)
|
|
880
|
+
}
|
|
881
|
+
case 'in':
|
|
882
|
+
case 'contains': {
|
|
883
|
+
const singletonSide = node.operator === 'in' ? left : right
|
|
884
|
+
const singletonSpan = node.operator === 'in' ? node.left.span : node.right.span
|
|
885
|
+
this.requireSingle(
|
|
886
|
+
singletonSide,
|
|
887
|
+
singletonSpan,
|
|
888
|
+
`The ${node.operator === 'in' ? 'left' : 'right'} operand of '${node.operator}' must be a single item`
|
|
889
|
+
)
|
|
890
|
+
return { types: ['System.Boolean'], single: true }
|
|
891
|
+
}
|
|
892
|
+
/* v8 ignore start -- the parser produces no other binary operators */
|
|
893
|
+
default:
|
|
894
|
+
return UNKNOWN
|
|
895
|
+
/* v8 ignore stop */
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/** Report a singleton violation when the state is statically known to be a collection. */
|
|
900
|
+
private requireSingle(state: StaticState, span: SourceSpan, message: string): void {
|
|
901
|
+
if (isCollection(state)) {
|
|
902
|
+
this.report('singleton-required', `${message}${NARROW_HINT}`, span)
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
private walkTypeOp(node: AstNode & { kind: 'typeOp' }, input: StaticState, scope: VariableScope): StaticState {
|
|
907
|
+
const operand = this.walk(node.operand, input, scope)
|
|
908
|
+
this.requireSingle(operand, node.operand.span, `'${node.operator}' expects a single item operand`)
|
|
909
|
+
const resolved = this.checkTypeName(node.type.parts, node.type.span)
|
|
910
|
+
if (node.operator === 'is') {
|
|
911
|
+
return { types: ['System.Boolean'], single: true }
|
|
912
|
+
}
|
|
913
|
+
if (resolved === undefined) {
|
|
914
|
+
return UNKNOWN
|
|
915
|
+
}
|
|
916
|
+
return { types: this.narrowTypes(operand, resolved, node.span), single: true }
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
private checkArithmetic(operator: string, left: StaticState, right: StaticState, span: SourceSpan): void {
|
|
920
|
+
if (isCollection(left) || isCollection(right)) {
|
|
921
|
+
this.report('singleton-required', `Operator '${operator}' expects single-item operands${NARROW_HINT}`, span)
|
|
922
|
+
return
|
|
923
|
+
}
|
|
924
|
+
const leftKind = kindOf(left)
|
|
925
|
+
const rightKind = kindOf(right)
|
|
926
|
+
if (leftKind === undefined || rightKind === undefined || leftKind === 'Complex' || rightKind === 'Complex') {
|
|
927
|
+
return
|
|
928
|
+
}
|
|
929
|
+
const scalars: ValueKind[] = ['Numeric', 'Quantity']
|
|
930
|
+
const valid =
|
|
931
|
+
(operator === '+' && leftKind === 'String' && rightKind === 'String') ||
|
|
932
|
+
((operator === '+' || operator === '-') &&
|
|
933
|
+
((leftKind === 'Numeric' && rightKind === 'Numeric') ||
|
|
934
|
+
(leftKind === 'Quantity' && rightKind === 'Quantity') ||
|
|
935
|
+
(leftKind === 'Temporal' && rightKind === 'Quantity'))) ||
|
|
936
|
+
((operator === '*' || operator === '/') && scalars.includes(leftKind) && scalars.includes(rightKind)) ||
|
|
937
|
+
((operator === 'div' || operator === 'mod') && leftKind === 'Numeric' && rightKind === 'Numeric')
|
|
938
|
+
if (!valid) {
|
|
939
|
+
this.report('operand-type', `Operator '${operator}' is not defined for these operand types`, span)
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
private checkComparable(left: StaticState, right: StaticState, span: SourceSpan): void {
|
|
944
|
+
const leftKind = kindOf(left)
|
|
945
|
+
const rightKind = kindOf(right)
|
|
946
|
+
if (leftKind === undefined || rightKind === undefined) {
|
|
947
|
+
return
|
|
948
|
+
}
|
|
949
|
+
if (leftKind === 'Boolean' || rightKind === 'Boolean' || leftKind !== rightKind) {
|
|
950
|
+
this.report('operand-type', 'Comparison operands must be single values of comparable types', span)
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
private checkEquality(left: StaticState, right: StaticState, span: SourceSpan): void {
|
|
955
|
+
const leftKind = kindOf(left)
|
|
956
|
+
const rightKind = kindOf(right)
|
|
957
|
+
if (leftKind === undefined || rightKind === undefined || leftKind === 'Complex' || rightKind === 'Complex') {
|
|
958
|
+
return
|
|
959
|
+
}
|
|
960
|
+
if (leftKind !== rightKind) {
|
|
961
|
+
this.report('equality-incompatible', `${leftKind} and ${rightKind} operands can never be equal (spec §11)`, span)
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
private requireKind(state: StaticState, kind: ValueKind, span: SourceSpan, message: string): void {
|
|
966
|
+
const actual = kindOf(state)
|
|
967
|
+
if (actual === undefined) {
|
|
968
|
+
return
|
|
969
|
+
}
|
|
970
|
+
const compatible = actual === kind || (kind === 'Numeric' && actual === 'Quantity')
|
|
971
|
+
if (!compatible) {
|
|
972
|
+
this.report('operand-type', `${message}, found ${state.types?.join(' | ') ?? 'unknown'}`, span)
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
private report(
|
|
977
|
+
code: string,
|
|
978
|
+
message: string,
|
|
979
|
+
span: SourceSpan,
|
|
980
|
+
severity: 'error' | 'warning' = 'error',
|
|
981
|
+
name?: string
|
|
982
|
+
): void {
|
|
983
|
+
this.diagnostics.push({ severity, code, message, span, ...(name !== undefined && { name }) })
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** Functions whose first argument is a regular expression pattern. */
|
|
988
|
+
const REGEX_PATTERN_FUNCTIONS = new Set(['matches', 'matchesFull', 'replaceMatches'])
|
|
989
|
+
|
|
990
|
+
const SYSTEM_TYPE_NAMES = new Set([
|
|
991
|
+
'Any',
|
|
992
|
+
'Boolean',
|
|
993
|
+
'String',
|
|
994
|
+
'Integer',
|
|
995
|
+
'Long',
|
|
996
|
+
'Decimal',
|
|
997
|
+
'Date',
|
|
998
|
+
'DateTime',
|
|
999
|
+
'Time',
|
|
1000
|
+
'Quantity',
|
|
1001
|
+
])
|
|
1002
|
+
|
|
1003
|
+
/** Appended to singleton-misuse messages so the fix is spelled out, not just the rule. */
|
|
1004
|
+
const NARROW_HINT = ' — narrow it to one item with first(), last(), or single()'
|
|
1005
|
+
|
|
1006
|
+
/** Statically known to possibly hold more than one item (types known, not a singleton). */
|
|
1007
|
+
function isCollection(state: StaticState): boolean {
|
|
1008
|
+
return state.types !== undefined && state.single === false
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* A host-supplied declaration as the analyzer uses it: arity and signature,
|
|
1013
|
+
* with an expression body's implicit zero arity already spelled out.
|
|
1014
|
+
*/
|
|
1015
|
+
interface ResolvedDeclaration {
|
|
1016
|
+
minArity?: number
|
|
1017
|
+
maxArity?: number
|
|
1018
|
+
signature?: CustomFunctionSignature
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function resolvedDeclaration(declared: SingleDeclaredFunction): ResolvedDeclaration {
|
|
1022
|
+
if (declared.expression === undefined) {
|
|
1023
|
+
return declared
|
|
1024
|
+
}
|
|
1025
|
+
// An expression-defined function takes no arguments, which is how the runtime
|
|
1026
|
+
// calls it (`evaluateHostFunction`).
|
|
1027
|
+
return { minArity: 0, maxArity: 0, ...(declared.signature !== undefined && { signature: declared.signature }) }
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* What several declarations of one name say together, for a call none of them
|
|
1032
|
+
* answers alone. Everything here widens: the arities span all of them, the
|
|
1033
|
+
* input is every type any of them accepts, and the result is the union of
|
|
1034
|
+
* theirs — unknown as soon as one declaration leaves it unknown. Arguments are
|
|
1035
|
+
* dropped, since checking them against the wrong declaration would report valid
|
|
1036
|
+
* code.
|
|
1037
|
+
*/
|
|
1038
|
+
function mergedDeclaration(candidates: readonly ResolvedDeclaration[]): ResolvedDeclaration {
|
|
1039
|
+
const maxArity = Math.max(...candidates.map(candidate => candidate.maxArity ?? Number.POSITIVE_INFINITY))
|
|
1040
|
+
const input = mergedInput(candidates)
|
|
1041
|
+
const result = mergedResult(candidates)
|
|
1042
|
+
return {
|
|
1043
|
+
minArity: Math.min(...candidates.map(candidate => candidate.minArity ?? 0)),
|
|
1044
|
+
...(Number.isFinite(maxArity) && { maxArity }),
|
|
1045
|
+
...((input !== undefined || result !== undefined) && {
|
|
1046
|
+
signature: { ...(input !== undefined && { input }), ...(result !== undefined && { result }) },
|
|
1047
|
+
}),
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/** Every type any declaration accepts, or undefined when one of them accepts anything. */
|
|
1052
|
+
function mergedInput(candidates: readonly ResolvedDeclaration[]): InputSpec | undefined {
|
|
1053
|
+
const declared = candidates.map(candidate => candidate.signature?.input?.types)
|
|
1054
|
+
if (declared.some(types => types === undefined)) {
|
|
1055
|
+
return undefined
|
|
1056
|
+
}
|
|
1057
|
+
return { types: [...new Set(declared.flatMap(types => types ?? []))] }
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/** The union of the declarations' results, unknown as soon as one of them is. */
|
|
1061
|
+
function mergedResult(candidates: readonly ResolvedDeclaration[]): { types?: string[]; single?: boolean } | undefined {
|
|
1062
|
+
const union = unionStates(
|
|
1063
|
+
candidates.map(candidate => ({
|
|
1064
|
+
types: candidate.signature?.result?.types,
|
|
1065
|
+
single: candidate.signature?.result?.single,
|
|
1066
|
+
}))
|
|
1067
|
+
)
|
|
1068
|
+
if (union.types === undefined && union.single === undefined) {
|
|
1069
|
+
return undefined
|
|
1070
|
+
}
|
|
1071
|
+
return {
|
|
1072
|
+
...(union.types !== undefined && { types: union.types }),
|
|
1073
|
+
...(union.single !== undefined && { single: union.single }),
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/** The behavior kind shared by every candidate type, or undefined when mixed/unknown. */
|
|
1078
|
+
function kindOf(state: StaticState): ValueKind | undefined {
|
|
1079
|
+
if (state.types === undefined || state.types.length === 0) {
|
|
1080
|
+
return undefined
|
|
1081
|
+
}
|
|
1082
|
+
let kind: ValueKind | undefined
|
|
1083
|
+
for (const type of state.types) {
|
|
1084
|
+
const typeKind = valueKindOfTypeName(type)
|
|
1085
|
+
if (kind === undefined) {
|
|
1086
|
+
kind = typeKind
|
|
1087
|
+
} else if (kind !== typeKind) {
|
|
1088
|
+
return undefined
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return kind
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function typeSpecifierParts(node: AstNode): string[] | undefined {
|
|
1095
|
+
if (node.kind === 'identifier') {
|
|
1096
|
+
return [node.name]
|
|
1097
|
+
}
|
|
1098
|
+
if (node.kind === 'dot' && node.left.kind === 'identifier' && node.right.kind === 'identifier') {
|
|
1099
|
+
return [node.left.name, node.right.name]
|
|
1100
|
+
}
|
|
1101
|
+
return undefined
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* `— did you mean 'X'?` for a mistyped name, or `''` when nothing is close enough.
|
|
1106
|
+
*/
|
|
1107
|
+
function didYouMean(target: string, candidates: Iterable<string>): string {
|
|
1108
|
+
const nearest = nearestName(target, candidates)
|
|
1109
|
+
return nearest === undefined ? '' : ` — did you mean '${nearest}'?`
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* The candidate `target` most plausibly misspells, or undefined when none is
|
|
1114
|
+
* close enough: a small edit budget scaled to the name's length, so a genuine
|
|
1115
|
+
* typo (`gven` → `given`, `lengthx` → `length`) matches while an unrelated name
|
|
1116
|
+
* (`nope`) does not. Both the `did you mean` suggestions and `analyzeSite`
|
|
1117
|
+
* weighing an unresolved column name go through here, so one budget decides what
|
|
1118
|
+
* counts as a plausible misspelling.
|
|
1119
|
+
*/
|
|
1120
|
+
function nearestName(target: string, candidates: Iterable<string>): string | undefined {
|
|
1121
|
+
// Budget grows with the name: 1 edit for short names (<=4), up to 3 for long ones.
|
|
1122
|
+
// Keeps suggestions high-precision — a real typo, not any name that happens to be near.
|
|
1123
|
+
const budget = Math.min(3, Math.ceil(target.length / 4))
|
|
1124
|
+
let best: string | undefined
|
|
1125
|
+
let bestDistance = budget + 1
|
|
1126
|
+
for (const candidate of candidates) {
|
|
1127
|
+
if (candidate === target) {
|
|
1128
|
+
continue
|
|
1129
|
+
}
|
|
1130
|
+
// Only a strictly better distance matters, so the cap tightens as matches are found.
|
|
1131
|
+
const limit = bestDistance - 1
|
|
1132
|
+
const distance = boundedEditDistance(target, candidate, limit)
|
|
1133
|
+
if (distance <= limit) {
|
|
1134
|
+
bestDistance = distance
|
|
1135
|
+
best = candidate
|
|
1136
|
+
if (bestDistance === 1) {
|
|
1137
|
+
break // Unbeatable: distance 0 would mean an exact match, which is skipped.
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return best
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Levenshtein distance capped at `limit`: exact when the distance is <= limit,
|
|
1146
|
+
* otherwise returns limit + 1. Only the |i - j| <= limit diagonal band of each
|
|
1147
|
+
* DP row is computed (cells outside it can never end <= limit), and the scan
|
|
1148
|
+
* bails as soon as a whole row exceeds the cap — so a non-match costs
|
|
1149
|
+
* O(limit^2) instead of a full O(len(a) * len(b)) table.
|
|
1150
|
+
*/
|
|
1151
|
+
function boundedEditDistance(a: string, b: string, limit: number): number {
|
|
1152
|
+
if (Math.abs(a.length - b.length) > limit) {
|
|
1153
|
+
return limit + 1
|
|
1154
|
+
}
|
|
1155
|
+
const previous: number[] = new Array(b.length + 1)
|
|
1156
|
+
for (let j = 0; j <= b.length; j++) {
|
|
1157
|
+
previous[j] = j
|
|
1158
|
+
}
|
|
1159
|
+
for (let i = 1; i <= a.length; i++) {
|
|
1160
|
+
const from = Math.max(1, i - limit)
|
|
1161
|
+
const to = Math.min(b.length, i + limit)
|
|
1162
|
+
let diagonal = previous[from - 1] as number
|
|
1163
|
+
previous[from - 1] = from === 1 ? i : limit + 1
|
|
1164
|
+
let rowBest = previous[from - 1] as number
|
|
1165
|
+
for (let j = from; j <= to; j++) {
|
|
1166
|
+
// The cell above sits outside the previous row's band when j = i + limit.
|
|
1167
|
+
const above = j > i - 1 + limit ? limit + 1 : (previous[j] as number)
|
|
1168
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
|
1169
|
+
const value = Math.min(above + 1, (previous[j - 1] as number) + 1, diagonal + cost)
|
|
1170
|
+
previous[j] = value
|
|
1171
|
+
diagonal = above
|
|
1172
|
+
if (value < rowBest) {
|
|
1173
|
+
rowBest = value
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
if (rowBest > limit) {
|
|
1177
|
+
return limit + 1
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return Math.min(previous[b.length] as number, limit + 1)
|
|
1181
|
+
}
|