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.
Files changed (414) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +391 -0
  3. package/THIRD-PARTY-NOTICES.md +296 -0
  4. package/dist/analyzer/analyze-dto.d.ts +45 -0
  5. package/dist/analyzer/analyze-dto.d.ts.map +1 -0
  6. package/dist/analyzer/analyze-dto.js +147 -0
  7. package/dist/analyzer/analyze-dto.js.map +1 -0
  8. package/dist/analyzer/analyze.d.ts +98 -0
  9. package/dist/analyzer/analyze.d.ts.map +1 -0
  10. package/dist/analyzer/analyze.js +925 -0
  11. package/dist/analyzer/analyze.js.map +1 -0
  12. package/dist/analyzer/expression-policy.d.ts +210 -0
  13. package/dist/analyzer/expression-policy.d.ts.map +1 -0
  14. package/dist/analyzer/expression-policy.js +254 -0
  15. package/dist/analyzer/expression-policy.js.map +1 -0
  16. package/dist/analyzer/index.d.ts +3 -0
  17. package/dist/analyzer/index.d.ts.map +1 -0
  18. package/dist/analyzer/index.js +6 -0
  19. package/dist/analyzer/index.js.map +1 -0
  20. package/dist/analyzer/regex-safety.d.ts +10 -0
  21. package/dist/analyzer/regex-safety.d.ts.map +1 -0
  22. package/dist/analyzer/regex-safety.js +84 -0
  23. package/dist/analyzer/regex-safety.js.map +1 -0
  24. package/dist/analyzer/signatures.d.ts +72 -0
  25. package/dist/analyzer/signatures.d.ts.map +1 -0
  26. package/dist/analyzer/signatures.js +195 -0
  27. package/dist/analyzer/signatures.js.map +1 -0
  28. package/dist/api/bundle.d.ts +29 -0
  29. package/dist/api/bundle.d.ts.map +1 -0
  30. package/dist/api/bundle.js +108 -0
  31. package/dist/api/bundle.js.map +1 -0
  32. package/dist/api/cache.d.ts +10 -0
  33. package/dist/api/cache.d.ts.map +1 -0
  34. package/dist/api/cache.js +38 -0
  35. package/dist/api/cache.js.map +1 -0
  36. package/dist/api/column-signature.d.ts +53 -0
  37. package/dist/api/column-signature.d.ts.map +1 -0
  38. package/dist/api/column-signature.js +43 -0
  39. package/dist/api/column-signature.js.map +1 -0
  40. package/dist/api/compile.d.ts +120 -0
  41. package/dist/api/compile.d.ts.map +1 -0
  42. package/dist/api/compile.js +167 -0
  43. package/dist/api/compile.js.map +1 -0
  44. package/dist/api/constraints.d.ts +48 -0
  45. package/dist/api/constraints.d.ts.map +1 -0
  46. package/dist/api/constraints.js +52 -0
  47. package/dist/api/constraints.js.map +1 -0
  48. package/dist/api/dto.d.ts +134 -0
  49. package/dist/api/dto.d.ts.map +1 -0
  50. package/dist/api/dto.js +295 -0
  51. package/dist/api/dto.js.map +1 -0
  52. package/dist/api/engine.d.ts +136 -0
  53. package/dist/api/engine.d.ts.map +1 -0
  54. package/dist/api/engine.js +183 -0
  55. package/dist/api/engine.js.map +1 -0
  56. package/dist/api/evaluate.d.ts +7 -0
  57. package/dist/api/evaluate.d.ts.map +1 -0
  58. package/dist/api/evaluate.js +10 -0
  59. package/dist/api/evaluate.js.map +1 -0
  60. package/dist/api/project.d.ts +89 -0
  61. package/dist/api/project.d.ts.map +1 -0
  62. package/dist/api/project.js +150 -0
  63. package/dist/api/project.js.map +1 -0
  64. package/dist/api/tagged.d.ts +14 -0
  65. package/dist/api/tagged.d.ts.map +1 -0
  66. package/dist/api/tagged.js +13 -0
  67. package/dist/api/tagged.js.map +1 -0
  68. package/dist/cli/dto-check.d.ts +25 -0
  69. package/dist/cli/dto-check.d.ts.map +1 -0
  70. package/dist/cli/dto-check.js +77 -0
  71. package/dist/cli/dto-check.js.map +1 -0
  72. package/dist/cli/fhirpath-check.d.ts +3 -0
  73. package/dist/cli/fhirpath-check.d.ts.map +1 -0
  74. package/dist/cli/fhirpath-check.js +149 -0
  75. package/dist/cli/fhirpath-check.js.map +1 -0
  76. package/dist/cli/ts-loader.d.mts +3 -0
  77. package/dist/cli/ts-loader.d.mts.map +1 -0
  78. package/dist/cli/ts-loader.mjs +48 -0
  79. package/dist/cli/ts-loader.mjs.map +1 -0
  80. package/dist/engine/context.d.ts +173 -0
  81. package/dist/engine/context.d.ts.map +1 -0
  82. package/dist/engine/context.js +118 -0
  83. package/dist/engine/context.js.map +1 -0
  84. package/dist/engine/evaluator.d.ts +7 -0
  85. package/dist/engine/evaluator.d.ts.map +1 -0
  86. package/dist/engine/evaluator.js +196 -0
  87. package/dist/engine/evaluator.js.map +1 -0
  88. package/dist/engine/navigation.d.ts +11 -0
  89. package/dist/engine/navigation.d.ts.map +1 -0
  90. package/dist/engine/navigation.js +102 -0
  91. package/dist/engine/navigation.js.map +1 -0
  92. package/dist/engine/operators/collections.d.ts +12 -0
  93. package/dist/engine/operators/collections.d.ts.map +1 -0
  94. package/dist/engine/operators/collections.js +31 -0
  95. package/dist/engine/operators/collections.js.map +1 -0
  96. package/dist/engine/operators/comparison.d.ts +13 -0
  97. package/dist/engine/operators/comparison.d.ts.map +1 -0
  98. package/dist/engine/operators/comparison.js +77 -0
  99. package/dist/engine/operators/comparison.js.map +1 -0
  100. package/dist/engine/operators/equality.d.ts +19 -0
  101. package/dist/engine/operators/equality.d.ts.map +1 -0
  102. package/dist/engine/operators/equality.js +203 -0
  103. package/dist/engine/operators/equality.js.map +1 -0
  104. package/dist/engine/operators/index.d.ts +18 -0
  105. package/dist/engine/operators/index.d.ts.map +1 -0
  106. package/dist/engine/operators/index.js +30 -0
  107. package/dist/engine/operators/index.js.map +1 -0
  108. package/dist/engine/operators/logic.d.ts +8 -0
  109. package/dist/engine/operators/logic.d.ts.map +1 -0
  110. package/dist/engine/operators/logic.js +38 -0
  111. package/dist/engine/operators/logic.js.map +1 -0
  112. package/dist/engine/operators/math.d.ts +13 -0
  113. package/dist/engine/operators/math.d.ts.map +1 -0
  114. package/dist/engine/operators/math.js +198 -0
  115. package/dist/engine/operators/math.js.map +1 -0
  116. package/dist/engine/operators/strings.d.ts +8 -0
  117. package/dist/engine/operators/strings.d.ts.map +1 -0
  118. package/dist/engine/operators/strings.js +22 -0
  119. package/dist/engine/operators/strings.js.map +1 -0
  120. package/dist/engine/operators/types.d.ts +3 -0
  121. package/dist/engine/operators/types.d.ts.map +1 -0
  122. package/dist/engine/operators/types.js +15 -0
  123. package/dist/engine/operators/types.js.map +1 -0
  124. package/dist/engine/type-matching.d.ts +20 -0
  125. package/dist/engine/type-matching.d.ts.map +1 -0
  126. package/dist/engine/type-matching.js +122 -0
  127. package/dist/engine/type-matching.js.map +1 -0
  128. package/dist/errors.d.ts +25 -0
  129. package/dist/errors.d.ts.map +1 -0
  130. package/dist/errors.js +21 -0
  131. package/dist/errors.js.map +1 -0
  132. package/dist/eslint/index.d.ts +12 -0
  133. package/dist/eslint/index.d.ts.map +1 -0
  134. package/dist/eslint/index.js +316 -0
  135. package/dist/eslint/index.js.map +1 -0
  136. package/dist/fhir/html-checks.d.ts +7 -0
  137. package/dist/fhir/html-checks.d.ts.map +1 -0
  138. package/dist/fhir/html-checks.js +256 -0
  139. package/dist/fhir/html-checks.js.map +1 -0
  140. package/dist/fhir/model-navigation.d.ts +11 -0
  141. package/dist/fhir/model-navigation.d.ts.map +1 -0
  142. package/dist/fhir/model-navigation.js +142 -0
  143. package/dist/fhir/model-navigation.js.map +1 -0
  144. package/dist/functions/aggregate.d.ts +2 -0
  145. package/dist/functions/aggregate.d.ts.map +1 -0
  146. package/dist/functions/aggregate.js +127 -0
  147. package/dist/functions/aggregate.js.map +1 -0
  148. package/dist/functions/boundary.d.ts +2 -0
  149. package/dist/functions/boundary.d.ts.map +1 -0
  150. package/dist/functions/boundary.js +189 -0
  151. package/dist/functions/boundary.js.map +1 -0
  152. package/dist/functions/combining.d.ts +2 -0
  153. package/dist/functions/combining.d.ts.map +1 -0
  154. package/dist/functions/combining.js +13 -0
  155. package/dist/functions/combining.js.map +1 -0
  156. package/dist/functions/control.d.ts +2 -0
  157. package/dist/functions/control.d.ts.map +1 -0
  158. package/dist/functions/control.js +25 -0
  159. package/dist/functions/control.js.map +1 -0
  160. package/dist/functions/conversion.d.ts +3 -0
  161. package/dist/functions/conversion.d.ts.map +1 -0
  162. package/dist/functions/conversion.js +316 -0
  163. package/dist/functions/conversion.js.map +1 -0
  164. package/dist/functions/date-components.d.ts +2 -0
  165. package/dist/functions/date-components.d.ts.map +1 -0
  166. package/dist/functions/date-components.js +81 -0
  167. package/dist/functions/date-components.js.map +1 -0
  168. package/dist/functions/existence.d.ts +4 -0
  169. package/dist/functions/existence.d.ts.map +1 -0
  170. package/dist/functions/existence.js +97 -0
  171. package/dist/functions/existence.js.map +1 -0
  172. package/dist/functions/fhir.d.ts +2 -0
  173. package/dist/functions/fhir.d.ts.map +1 -0
  174. package/dist/functions/fhir.js +222 -0
  175. package/dist/functions/fhir.js.map +1 -0
  176. package/dist/functions/filtering.d.ts +2 -0
  177. package/dist/functions/filtering.d.ts.map +1 -0
  178. package/dist/functions/filtering.js +112 -0
  179. package/dist/functions/filtering.js.map +1 -0
  180. package/dist/functions/install.d.ts +18 -0
  181. package/dist/functions/install.d.ts.map +1 -0
  182. package/dist/functions/install.js +20 -0
  183. package/dist/functions/install.js.map +1 -0
  184. package/dist/functions/iteration.d.ts +7 -0
  185. package/dist/functions/iteration.d.ts.map +1 -0
  186. package/dist/functions/iteration.js +9 -0
  187. package/dist/functions/iteration.js.map +1 -0
  188. package/dist/functions/math.d.ts +2 -0
  189. package/dist/functions/math.d.ts.map +1 -0
  190. package/dist/functions/math.js +152 -0
  191. package/dist/functions/math.js.map +1 -0
  192. package/dist/functions/reflection.d.ts +2 -0
  193. package/dist/functions/reflection.d.ts.map +1 -0
  194. package/dist/functions/reflection.js +23 -0
  195. package/dist/functions/reflection.js.map +1 -0
  196. package/dist/functions/registry.d.ts +28 -0
  197. package/dist/functions/registry.d.ts.map +1 -0
  198. package/dist/functions/registry.js +46 -0
  199. package/dist/functions/registry.js.map +1 -0
  200. package/dist/functions/sort.d.ts +2 -0
  201. package/dist/functions/sort.d.ts.map +1 -0
  202. package/dist/functions/sort.js +48 -0
  203. package/dist/functions/sort.js.map +1 -0
  204. package/dist/functions/string.d.ts +2 -0
  205. package/dist/functions/string.d.ts.map +1 -0
  206. package/dist/functions/string.js +353 -0
  207. package/dist/functions/string.js.map +1 -0
  208. package/dist/functions/subsetting.d.ts +2 -0
  209. package/dist/functions/subsetting.d.ts.map +1 -0
  210. package/dist/functions/subsetting.js +72 -0
  211. package/dist/functions/subsetting.js.map +1 -0
  212. package/dist/functions/tree.d.ts +8 -0
  213. package/dist/functions/tree.d.ts.map +1 -0
  214. package/dist/functions/tree.js +109 -0
  215. package/dist/functions/tree.js.map +1 -0
  216. package/dist/functions/type-specifier.d.ts +8 -0
  217. package/dist/functions/type-specifier.d.ts.map +1 -0
  218. package/dist/functions/type-specifier.js +27 -0
  219. package/dist/functions/type-specifier.js.map +1 -0
  220. package/dist/functions/utility.d.ts +2 -0
  221. package/dist/functions/utility.d.ts.map +1 -0
  222. package/dist/functions/utility.js +76 -0
  223. package/dist/functions/utility.js.map +1 -0
  224. package/dist/functions/variables.d.ts +2 -0
  225. package/dist/functions/variables.d.ts.map +1 -0
  226. package/dist/functions/variables.js +35 -0
  227. package/dist/functions/variables.js.map +1 -0
  228. package/dist/index.d.ts +25 -0
  229. package/dist/index.d.ts.map +1 -0
  230. package/dist/index.js +11 -0
  231. package/dist/index.js.map +1 -0
  232. package/dist/lexer/lexer.d.ts +4 -0
  233. package/dist/lexer/lexer.d.ts.map +1 -0
  234. package/dist/lexer/lexer.js +318 -0
  235. package/dist/lexer/lexer.js.map +1 -0
  236. package/dist/lexer/tokens.d.ts +23 -0
  237. package/dist/lexer/tokens.d.ts.map +1 -0
  238. package/dist/lexer/tokens.js +40 -0
  239. package/dist/lexer/tokens.js.map +1 -0
  240. package/dist/model/provider.d.ts +36 -0
  241. package/dist/model/provider.d.ts.map +1 -0
  242. package/dist/model/provider.js +2 -0
  243. package/dist/model/provider.js.map +1 -0
  244. package/dist/parser/ast.d.ts +104 -0
  245. package/dist/parser/ast.d.ts.map +1 -0
  246. package/dist/parser/ast.js +2 -0
  247. package/dist/parser/ast.js.map +1 -0
  248. package/dist/parser/parser.d.ts +4 -0
  249. package/dist/parser/parser.d.ts.map +1 -0
  250. package/dist/parser/parser.js +299 -0
  251. package/dist/parser/parser.js.map +1 -0
  252. package/dist/parser/precedence.d.ts +24 -0
  253. package/dist/parser/precedence.d.ts.map +1 -0
  254. package/dist/parser/precedence.js +52 -0
  255. package/dist/parser/precedence.js.map +1 -0
  256. package/dist/parser/printer.d.ts +4 -0
  257. package/dist/parser/printer.d.ts.map +1 -0
  258. package/dist/parser/printer.js +112 -0
  259. package/dist/parser/printer.js.map +1 -0
  260. package/dist/r4/generated/resources-data.d.ts +3 -0
  261. package/dist/r4/generated/resources-data.d.ts.map +1 -0
  262. package/dist/r4/generated/resources-data.js +28628 -0
  263. package/dist/r4/generated/resources-data.js.map +1 -0
  264. package/dist/r4/generated/type-maps.d.ts +43153 -0
  265. package/dist/r4/generated/type-maps.d.ts.map +1 -0
  266. package/dist/r4/generated/type-maps.js +4 -0
  267. package/dist/r4/generated/type-maps.js.map +1 -0
  268. package/dist/r4/generated/types-data.d.ts +3 -0
  269. package/dist/r4/generated/types-data.d.ts.map +1 -0
  270. package/dist/r4/generated/types-data.js +2205 -0
  271. package/dist/r4/generated/types-data.js.map +1 -0
  272. package/dist/r4/index.d.ts +9 -0
  273. package/dist/r4/index.d.ts.map +1 -0
  274. package/dist/r4/index.js +94 -0
  275. package/dist/r4/index.js.map +1 -0
  276. package/dist/r4/model-data.d.ts +17 -0
  277. package/dist/r4/model-data.d.ts.map +1 -0
  278. package/dist/r4/model-data.js +2 -0
  279. package/dist/r4/model-data.js.map +1 -0
  280. package/dist/sites/index.d.ts +31 -0
  281. package/dist/sites/index.d.ts.map +1 -0
  282. package/dist/sites/index.js +205 -0
  283. package/dist/sites/index.js.map +1 -0
  284. package/dist/typed/infer.d.ts +234 -0
  285. package/dist/typed/infer.d.ts.map +1 -0
  286. package/dist/typed/infer.js +85 -0
  287. package/dist/typed/infer.js.map +1 -0
  288. package/dist/typed/perf-fixture.types.d.ts +136 -0
  289. package/dist/typed/perf-fixture.types.d.ts.map +1 -0
  290. package/dist/typed/perf-fixture.types.js +2 -0
  291. package/dist/typed/perf-fixture.types.js.map +1 -0
  292. package/dist/values/collection.d.ts +24 -0
  293. package/dist/values/collection.d.ts.map +1 -0
  294. package/dist/values/collection.js +51 -0
  295. package/dist/values/collection.js.map +1 -0
  296. package/dist/values/datetime.d.ts +54 -0
  297. package/dist/values/datetime.d.ts.map +1 -0
  298. package/dist/values/datetime.js +226 -0
  299. package/dist/values/datetime.js.map +1 -0
  300. package/dist/values/decimal.d.ts +48 -0
  301. package/dist/values/decimal.d.ts.map +1 -0
  302. package/dist/values/decimal.js +188 -0
  303. package/dist/values/decimal.js.map +1 -0
  304. package/dist/values/numeric.d.ts +24 -0
  305. package/dist/values/numeric.d.ts.map +1 -0
  306. package/dist/values/numeric.js +53 -0
  307. package/dist/values/numeric.js.map +1 -0
  308. package/dist/values/quantity.d.ts +49 -0
  309. package/dist/values/quantity.d.ts.map +1 -0
  310. package/dist/values/quantity.js +297 -0
  311. package/dist/values/quantity.js.map +1 -0
  312. package/dist/values/temporal-arithmetic.d.ts +17 -0
  313. package/dist/values/temporal-arithmetic.d.ts.map +1 -0
  314. package/dist/values/temporal-arithmetic.js +183 -0
  315. package/dist/values/temporal-arithmetic.js.map +1 -0
  316. package/dist/values/temporal-compare.d.ts +9 -0
  317. package/dist/values/temporal-compare.d.ts.map +1 -0
  318. package/dist/values/temporal-compare.js +59 -0
  319. package/dist/values/temporal-compare.js.map +1 -0
  320. package/dist/values/type-compat.d.ts +47 -0
  321. package/dist/values/type-compat.d.ts.map +1 -0
  322. package/dist/values/type-compat.js +100 -0
  323. package/dist/values/type-compat.js.map +1 -0
  324. package/dist/values/typed-value.d.ts +48 -0
  325. package/dist/values/typed-value.d.ts.map +1 -0
  326. package/dist/values/typed-value.js +117 -0
  327. package/dist/values/typed-value.js.map +1 -0
  328. package/dist/values/ucum.d.ts +14 -0
  329. package/dist/values/ucum.d.ts.map +1 -0
  330. package/dist/values/ucum.js +146 -0
  331. package/dist/values/ucum.js.map +1 -0
  332. package/package.json +137 -0
  333. package/src/analyzer/analyze-dto.ts +195 -0
  334. package/src/analyzer/analyze.ts +1181 -0
  335. package/src/analyzer/expression-policy.ts +434 -0
  336. package/src/analyzer/index.ts +23 -0
  337. package/src/analyzer/regex-safety.ts +85 -0
  338. package/src/analyzer/signatures.ts +260 -0
  339. package/src/api/bundle.ts +132 -0
  340. package/src/api/cache.ts +40 -0
  341. package/src/api/column-signature.ts +71 -0
  342. package/src/api/compile.ts +308 -0
  343. package/src/api/constraints.ts +104 -0
  344. package/src/api/dto.ts +445 -0
  345. package/src/api/engine.ts +328 -0
  346. package/src/api/evaluate.ts +11 -0
  347. package/src/api/project.ts +255 -0
  348. package/src/api/tagged.ts +35 -0
  349. package/src/cli/dto-check.ts +94 -0
  350. package/src/cli/fhirpath-check.ts +168 -0
  351. package/src/cli/ts-loader.mjs +49 -0
  352. package/src/engine/context.ts +282 -0
  353. package/src/engine/evaluator.ts +246 -0
  354. package/src/engine/navigation.ts +106 -0
  355. package/src/engine/operators/collections.ts +34 -0
  356. package/src/engine/operators/comparison.ts +82 -0
  357. package/src/engine/operators/equality.ts +232 -0
  358. package/src/engine/operators/index.ts +65 -0
  359. package/src/engine/operators/logic.ts +53 -0
  360. package/src/engine/operators/math.ts +225 -0
  361. package/src/engine/operators/strings.ts +25 -0
  362. package/src/engine/operators/types.ts +16 -0
  363. package/src/engine/type-matching.ts +148 -0
  364. package/src/errors.ts +33 -0
  365. package/src/eslint/index.ts +388 -0
  366. package/src/fhir/html-checks.ts +266 -0
  367. package/src/fhir/model-navigation.ts +148 -0
  368. package/src/functions/aggregate.ts +136 -0
  369. package/src/functions/boundary.ts +209 -0
  370. package/src/functions/combining.ts +15 -0
  371. package/src/functions/control.ts +25 -0
  372. package/src/functions/conversion.ts +349 -0
  373. package/src/functions/date-components.ts +88 -0
  374. package/src/functions/existence.ts +107 -0
  375. package/src/functions/fhir.ts +247 -0
  376. package/src/functions/filtering.ts +120 -0
  377. package/src/functions/install.ts +19 -0
  378. package/src/functions/iteration.ts +21 -0
  379. package/src/functions/math.ts +167 -0
  380. package/src/functions/reflection.ts +24 -0
  381. package/src/functions/registry.ts +72 -0
  382. package/src/functions/sort.ts +68 -0
  383. package/src/functions/string.ts +413 -0
  384. package/src/functions/subsetting.ts +89 -0
  385. package/src/functions/tree.ts +112 -0
  386. package/src/functions/type-specifier.ts +29 -0
  387. package/src/functions/utility.ts +82 -0
  388. package/src/functions/variables.ts +36 -0
  389. package/src/index.ts +30 -0
  390. package/src/lexer/lexer.ts +341 -0
  391. package/src/lexer/tokens.ts +69 -0
  392. package/src/model/provider.ts +36 -0
  393. package/src/parser/ast.ts +164 -0
  394. package/src/parser/parser.ts +321 -0
  395. package/src/parser/precedence.ts +52 -0
  396. package/src/parser/printer.ts +114 -0
  397. package/src/r4/generated/resources-data.ts +28631 -0
  398. package/src/r4/generated/type-maps.ts +22145 -0
  399. package/src/r4/generated/types-data.ts +2208 -0
  400. package/src/r4/index.ts +107 -0
  401. package/src/r4/model-data.ts +17 -0
  402. package/src/sites/index.ts +285 -0
  403. package/src/typed/infer.ts +433 -0
  404. package/src/typed/perf-fixture.types.ts +143 -0
  405. package/src/values/collection.ts +57 -0
  406. package/src/values/datetime.ts +285 -0
  407. package/src/values/decimal.ts +213 -0
  408. package/src/values/numeric.ts +63 -0
  409. package/src/values/quantity.ts +358 -0
  410. package/src/values/temporal-arithmetic.ts +231 -0
  411. package/src/values/temporal-compare.ts +65 -0
  412. package/src/values/type-compat.ts +133 -0
  413. package/src/values/typed-value.ts +151 -0
  414. 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
+ }