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