@cdxgen/cdxgen 13.2.0 → 13.4.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 (311) hide show
  1. package/README.md +33 -14
  2. package/bin/cdxgen.js +310 -138
  3. package/bin/evinse.js +22 -0
  4. package/bin/repl.js +86 -0
  5. package/bin/sign.js +119 -15
  6. package/bin/tracebom.js +69 -5
  7. package/bin/validate.js +107 -19
  8. package/bin/verify.js +172 -63
  9. package/data/helpers/init.gradle +46 -21
  10. package/data/remediations.json +85 -0
  11. package/data/rules/dependency-sources.yaml +67 -0
  12. package/data/rules/scala-evidence.yaml +115 -0
  13. package/lib/audit/index.js +38 -6
  14. package/lib/cli/bomAssembly.js +105 -15
  15. package/lib/cli/cliOptions.js +93 -1
  16. package/lib/cli/index.js +156 -58
  17. package/lib/cli/jsBom.js +99 -53
  18. package/lib/cli/jvmBom.js +708 -147
  19. package/lib/cli/managedBom.js +415 -224
  20. package/lib/cli/nativeBom.js +264 -44
  21. package/lib/core/activity.js +12 -0
  22. package/lib/core/env.js +22 -15
  23. package/lib/core/fs.js +176 -7
  24. package/lib/core/httpClient.js +159 -24
  25. package/lib/core/metadataProperties.js +97 -0
  26. package/lib/core/pluginRun.js +184 -0
  27. package/lib/core/processTree.js +100 -0
  28. package/lib/core/projectConfig.js +31 -4
  29. package/lib/core/runState.js +39 -0
  30. package/lib/core/toml.js +72 -0
  31. package/lib/ecosystems/asarutils.js +4 -2
  32. package/lib/ecosystems/bunutils.js +274 -108
  33. package/lib/ecosystems/cargoCache.js +231 -8
  34. package/lib/ecosystems/caxa.js +434 -2
  35. package/lib/ecosystems/cmakeResolver.js +76 -21
  36. package/lib/ecosystems/cppBuildContext.js +631 -0
  37. package/lib/ecosystems/cppEvidence.js +399 -51
  38. package/lib/ecosystems/cppIncludeResolver.js +294 -0
  39. package/lib/ecosystems/denoutils.js +93 -66
  40. package/lib/ecosystems/ecosystems.js +1983 -793
  41. package/lib/ecosystems/golem.js +28 -12
  42. package/lib/ecosystems/gradleutils.js +356 -37
  43. package/lib/ecosystems/jsEvidence.js +104 -44
  44. package/lib/ecosystems/kosi.js +31 -7
  45. package/lib/ecosystems/npmutils.js +174 -9
  46. package/lib/ecosystems/parsers-dotnet.js +616 -159
  47. package/lib/ecosystems/parsers-elm.js +46 -28
  48. package/lib/ecosystems/parsers-gleam.js +3 -4
  49. package/lib/ecosystems/parsers-go.js +371 -70
  50. package/lib/ecosystems/parsers-js.js +263 -76
  51. package/lib/ecosystems/parsers-julia.js +2 -3
  52. package/lib/ecosystems/parsers-jvm.js +204 -12
  53. package/lib/ecosystems/parsers-misc.js +929 -216
  54. package/lib/ecosystems/parsers-python.js +167 -24
  55. package/lib/ecosystems/parsers-rust.js +28 -22
  56. package/lib/ecosystems/parsers-terraform.js +1573 -19
  57. package/lib/ecosystems/pythonutils.js +102 -6
  58. package/lib/ecosystems/remote/huggingface.js +3 -0
  59. package/lib/ecosystems/rubyutils.js +1492 -182
  60. package/lib/ecosystems/rusi.js +54 -10
  61. package/lib/ecosystems/sbtutils.js +243 -187
  62. package/lib/ecosystems/scalacliutils.js +342 -0
  63. package/lib/ecosystems/scalasem.js +2618 -0
  64. package/lib/ecosystems/terraformRegistry.js +288 -0
  65. package/lib/ecosystems/utils.js +2 -0
  66. package/lib/ecosystems/vendoredCode.js +156 -0
  67. package/lib/evinser/evinser.js +1008 -191
  68. package/lib/evinser/swiftsem.js +1914 -302
  69. package/lib/helpers/agenticToolutils.js +2 -2
  70. package/lib/helpers/bomSigner.js +1101 -222
  71. package/lib/inventory/aiCollector.js +4 -1
  72. package/lib/inventory/analyzer.js +110 -127
  73. package/lib/inventory/atomUtils.js +703 -43
  74. package/lib/inventory/auditCategories.js +7 -0
  75. package/lib/inventory/buildToolRateLimit.js +144 -0
  76. package/lib/inventory/cacheCommands.js +2 -1
  77. package/lib/inventory/cdxrs.js +103 -12
  78. package/lib/inventory/cmakeBuildDirs.js +254 -0
  79. package/lib/inventory/deps.js +501 -124
  80. package/lib/inventory/depsUtils.js +183 -11
  81. package/lib/inventory/display.js +4 -5
  82. package/lib/inventory/dosai.js +798 -57
  83. package/lib/inventory/dosaiParsers.js +310 -15
  84. package/lib/inventory/dynamic.js +90 -6
  85. package/lib/inventory/evidenceUtils.js +166 -113
  86. package/lib/inventory/fetchBatch.js +690 -55
  87. package/lib/inventory/fetchDiskCache.js +597 -0
  88. package/lib/inventory/forgeEnricher.js +14 -5
  89. package/lib/inventory/jvmLocalRepos.js +917 -0
  90. package/lib/inventory/mcpConfigParser.js +2 -3
  91. package/lib/inventory/parentSelection.js +146 -0
  92. package/lib/inventory/protobom.js +8 -1
  93. package/lib/inventory/purl.js +152 -3
  94. package/lib/inventory/rustFormulationParser.js +3 -4
  95. package/lib/inventory/sbomDocument.js +2 -70
  96. package/lib/inventory/scalaCoords.js +153 -0
  97. package/lib/inventory/source.js +45 -27
  98. package/lib/inventory/spdx.js +101 -0
  99. package/lib/inventory/traceRunner.js +295 -25
  100. package/lib/inventory/tracedCrypto.js +294 -0
  101. package/lib/managers/binary.js +115 -24
  102. package/lib/managers/docker.js +467 -46
  103. package/lib/managers/oci.js +63 -3
  104. package/lib/parsers/cmakeCache.js +69 -0
  105. package/lib/parsers/cmakeLists.js +466 -0
  106. package/lib/parsers/cmakePresets.js +400 -0
  107. package/lib/parsers/compileCommands.js +557 -0
  108. package/lib/parsers/largeJson.js +454 -0
  109. package/lib/parsers/npmrc.js +19 -1
  110. package/lib/parsers/strictJson.js +322 -0
  111. package/lib/parsers/terraformConfig.js +1073 -0
  112. package/lib/parsers/terraformSource.js +652 -0
  113. package/lib/server/server.js +23 -0
  114. package/lib/stages/postgen/introspection/markers.js +1 -1
  115. package/lib/stages/postgen/postgen.js +83 -16
  116. package/lib/stages/postgen/specVersionCompat.js +32 -0
  117. package/lib/validator/bomValidator.js +6 -16
  118. package/lib/validator/index.js +64 -11
  119. package/lib/validator/reporters/console.js +3 -0
  120. package/lib/validator/reporters/json.js +2 -0
  121. package/lib/validator/reporters/sarif.js +7 -0
  122. package/package.json +23 -23
  123. package/types/lib/audit/index.d.ts.map +1 -1
  124. package/types/lib/cli/bomAssembly.d.ts +2 -1
  125. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  126. package/types/lib/cli/cliOptions.d.ts +33 -1
  127. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  128. package/types/lib/cli/index.d.ts.map +1 -1
  129. package/types/lib/cli/jsBom.d.ts.map +1 -1
  130. package/types/lib/cli/jvmBom.d.ts +24 -1
  131. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  132. package/types/lib/cli/managedBom.d.ts +7 -4
  133. package/types/lib/cli/managedBom.d.ts.map +1 -1
  134. package/types/lib/cli/nativeBom.d.ts +3 -1
  135. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  136. package/types/lib/core/activity.d.ts.map +1 -1
  137. package/types/lib/core/env.d.ts +9 -0
  138. package/types/lib/core/env.d.ts.map +1 -1
  139. package/types/lib/core/fs.d.ts +55 -0
  140. package/types/lib/core/fs.d.ts.map +1 -1
  141. package/types/lib/core/httpClient.d.ts +13 -8
  142. package/types/lib/core/httpClient.d.ts.map +1 -1
  143. package/types/lib/core/metadataProperties.d.ts +39 -0
  144. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  145. package/types/lib/core/pluginRun.d.ts +97 -0
  146. package/types/lib/core/pluginRun.d.ts.map +1 -0
  147. package/types/lib/core/processTree.d.ts +36 -0
  148. package/types/lib/core/processTree.d.ts.map +1 -0
  149. package/types/lib/core/projectConfig.d.ts +6 -2
  150. package/types/lib/core/projectConfig.d.ts.map +1 -1
  151. package/types/lib/core/runState.d.ts +29 -0
  152. package/types/lib/core/runState.d.ts.map +1 -0
  153. package/types/lib/core/toml.d.ts +20 -0
  154. package/types/lib/core/toml.d.ts.map +1 -0
  155. package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
  156. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  157. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  158. package/types/lib/ecosystems/cargoCache.d.ts +12 -0
  159. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
  160. package/types/lib/ecosystems/caxa.d.ts +14 -0
  161. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  162. package/types/lib/ecosystems/cmakeResolver.d.ts +28 -3
  163. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  164. package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
  165. package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
  166. package/types/lib/ecosystems/cppEvidence.d.ts +17 -1
  167. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  168. package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
  169. package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
  170. package/types/lib/ecosystems/denoutils.d.ts +9 -3
  171. package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
  172. package/types/lib/ecosystems/ecosystems.d.ts +62 -13
  173. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  174. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  175. package/types/lib/ecosystems/gradleutils.d.ts +70 -4
  176. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  177. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  178. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  179. package/types/lib/ecosystems/kosi.d.ts +13 -0
  180. package/types/lib/ecosystems/kosi.d.ts.map +1 -1
  181. package/types/lib/ecosystems/npmutils.d.ts +57 -0
  182. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  183. package/types/lib/ecosystems/parsers-dotnet.d.ts +118 -4
  184. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  185. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
  186. package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
  187. package/types/lib/ecosystems/parsers-go.d.ts +54 -2
  188. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  189. package/types/lib/ecosystems/parsers-js.d.ts +16 -5
  190. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  191. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
  192. package/types/lib/ecosystems/parsers-jvm.d.ts +46 -1
  193. package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
  194. package/types/lib/ecosystems/parsers-misc.d.ts +70 -4
  195. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  196. package/types/lib/ecosystems/parsers-python.d.ts +53 -3
  197. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  198. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  199. package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
  200. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
  201. package/types/lib/ecosystems/pythonutils.d.ts +27 -2
  202. package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
  203. package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
  204. package/types/lib/ecosystems/rubyutils.d.ts +164 -3
  205. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  206. package/types/lib/ecosystems/rusi.d.ts +13 -0
  207. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  208. package/types/lib/ecosystems/sbtutils.d.ts +88 -2
  209. package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
  210. package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
  211. package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
  212. package/types/lib/ecosystems/scalasem.d.ts +311 -0
  213. package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
  214. package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
  215. package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
  216. package/types/lib/ecosystems/utils.d.ts +2 -2
  217. package/types/lib/ecosystems/utils.d.ts.map +1 -1
  218. package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
  219. package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
  220. package/types/lib/evinser/evinser.d.ts +132 -15
  221. package/types/lib/evinser/evinser.d.ts.map +1 -1
  222. package/types/lib/evinser/swiftsem.d.ts +280 -15
  223. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  224. package/types/lib/helpers/bomSigner.d.ts +182 -13
  225. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  226. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  227. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  228. package/types/lib/inventory/atomUtils.d.ts +219 -5
  229. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  230. package/types/lib/inventory/auditCategories.d.ts +3 -0
  231. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  232. package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
  233. package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
  234. package/types/lib/inventory/cacheCommands.d.ts +2 -1
  235. package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
  236. package/types/lib/inventory/cdxrs.d.ts +8 -3
  237. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  238. package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
  239. package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
  240. package/types/lib/inventory/deps.d.ts +59 -1
  241. package/types/lib/inventory/deps.d.ts.map +1 -1
  242. package/types/lib/inventory/depsUtils.d.ts +15 -0
  243. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  244. package/types/lib/inventory/display.d.ts.map +1 -1
  245. package/types/lib/inventory/dosai.d.ts +121 -9
  246. package/types/lib/inventory/dosai.d.ts.map +1 -1
  247. package/types/lib/inventory/dosaiParsers.d.ts +53 -7
  248. package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
  249. package/types/lib/inventory/dynamic.d.ts +10 -0
  250. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  251. package/types/lib/inventory/evidenceUtils.d.ts +19 -4
  252. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  253. package/types/lib/inventory/fetchBatch.d.ts +83 -14
  254. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  255. package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
  256. package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
  257. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  258. package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
  259. package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
  260. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  261. package/types/lib/inventory/parentSelection.d.ts +34 -0
  262. package/types/lib/inventory/parentSelection.d.ts.map +1 -0
  263. package/types/lib/inventory/plugins.d.ts.map +1 -1
  264. package/types/lib/inventory/protobom.d.ts.map +1 -1
  265. package/types/lib/inventory/purl.d.ts +71 -2
  266. package/types/lib/inventory/purl.d.ts.map +1 -1
  267. package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
  268. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  269. package/types/lib/inventory/scalaCoords.d.ts +80 -0
  270. package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
  271. package/types/lib/inventory/source.d.ts.map +1 -1
  272. package/types/lib/inventory/spdx.d.ts +9 -0
  273. package/types/lib/inventory/spdx.d.ts.map +1 -1
  274. package/types/lib/inventory/traceRunner.d.ts +59 -1
  275. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  276. package/types/lib/inventory/tracedCrypto.d.ts +42 -0
  277. package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
  278. package/types/lib/managers/binary.d.ts +30 -1
  279. package/types/lib/managers/binary.d.ts.map +1 -1
  280. package/types/lib/managers/docker.d.ts +29 -0
  281. package/types/lib/managers/docker.d.ts.map +1 -1
  282. package/types/lib/managers/oci.d.ts +17 -1
  283. package/types/lib/managers/oci.d.ts.map +1 -1
  284. package/types/lib/parsers/cmakeCache.d.ts +16 -0
  285. package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
  286. package/types/lib/parsers/cmakeLists.d.ts +77 -0
  287. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  288. package/types/lib/parsers/cmakePresets.d.ts +78 -0
  289. package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
  290. package/types/lib/parsers/compileCommands.d.ts +115 -0
  291. package/types/lib/parsers/compileCommands.d.ts.map +1 -0
  292. package/types/lib/parsers/largeJson.d.ts +52 -0
  293. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  294. package/types/lib/parsers/npmrc.d.ts +5 -0
  295. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  296. package/types/lib/parsers/strictJson.d.ts +46 -0
  297. package/types/lib/parsers/strictJson.d.ts.map +1 -0
  298. package/types/lib/parsers/terraformConfig.d.ts +70 -0
  299. package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
  300. package/types/lib/parsers/terraformSource.d.ts +66 -0
  301. package/types/lib/parsers/terraformSource.d.ts.map +1 -0
  302. package/types/lib/server/server.d.ts.map +1 -1
  303. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  304. package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
  305. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  306. package/types/lib/validator/index.d.ts +18 -1
  307. package/types/lib/validator/index.d.ts.map +1 -1
  308. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  309. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  310. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
  311. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
@@ -1,17 +1,37 @@
1
- import { lstatSync, readFileSync } from "node:fs";
1
+ import {
2
+ lstatSync,
3
+ readdirSync,
4
+ readFileSync,
5
+ realpathSync,
6
+ statSync,
7
+ } from "node:fs";
2
8
  import { tmpdir, totalmem } from "node:os";
3
- import { delimiter as _delimiter, dirname, join, resolve } from "node:path";
9
+ import {
10
+ delimiter as _delimiter,
11
+ basename,
12
+ dirname,
13
+ join,
14
+ resolve,
15
+ } from "node:path";
4
16
  import process from "node:process";
5
17
 
6
- import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
18
+ import {
19
+ DEBUG_MODE,
20
+ isSecureMode,
21
+ readEnvironmentVariable,
22
+ } from "../core/activity.js";
7
23
  import {
8
24
  safeExistsSync,
9
25
  safeMkdtempSync,
10
26
  safeRmSync,
11
27
  safeSpawnSync,
28
+ TIMEOUT_MS,
12
29
  } from "../core/fs.js";
13
30
  import { TRACE_MODE } from "../core/logger.js";
14
31
  import { dirNameStr, isWin } from "../core/paths.js";
32
+ import { formatDuration, reapProcessTree } from "../core/processTree.js";
33
+ import { readJsonFile } from "../parsers/largeJson.js";
34
+ import { cmakeBuildDirCandidates } from "./cmakeBuildDirs.js";
15
35
 
16
36
  const ASTGEN_DEFAULT_IGNORE_DIRS = [
17
37
  "venv",
@@ -57,6 +77,243 @@ export const ATOM_NATIVE_PACKAGES = new Set([
57
77
 
58
78
  const ATOM_PHP_LANGUAGES = new Set(["php"]);
59
79
 
80
+ /** atom `-l` values that parse C/C++ headers without function bodies. */
81
+ const ATOM_HEADER_LANGUAGES = new Set(["h", "hpp", "i"]);
82
+
83
+ /** atom `-l` values handled by the C/C++ frontend. */
84
+ const ATOM_C_LANGUAGES = new Set([
85
+ "c",
86
+ "cpp",
87
+ "c++",
88
+ "newc",
89
+ ...ATOM_HEADER_LANGUAGES,
90
+ ]);
91
+
92
+ const COMPILE_COMMANDS_FILE = "compile_commands.json";
93
+
94
+ /**
95
+ * Characters a Windows shell command line would interpret: `&`, `|`, `<` and
96
+ * `>` chain or redirect commands, `%` expands variables even inside quotes,
97
+ * `^` escapes, `!` expands under delayed expansion, and a quote breaks the
98
+ * quoting Node applies to arguments when `shell` is on. A database is often
99
+ * discovered inside the scanned tree, so its path is not trusted input.
100
+ */
101
+ const UNSAFE_SHELL_CHARS_RE = /[\r\n\t&|<>%^!"]/;
102
+
103
+ function compileCommandsIn(dir) {
104
+ for (const candidate of [
105
+ join(dir, COMPILE_COMMANDS_FILE),
106
+ join(dir, "build", COMPILE_COMMANDS_FILE),
107
+ ]) {
108
+ try {
109
+ if (statSync(candidate).isFile()) {
110
+ return candidate;
111
+ }
112
+ } catch (_err) {
113
+ // not there
114
+ }
115
+ }
116
+ return undefined;
117
+ }
118
+
119
+ /**
120
+ * Locate the JSON compilation database (`compile_commands.json`) of a C/C++
121
+ * project, which lets atom parse each file with the include paths, macros and
122
+ * language its build uses.
123
+ *
124
+ * An explicit `options.compileCommands` (a file, or a directory holding one
125
+ * directly or under `build/`) wins. Otherwise the scan root and the project's
126
+ * build directories are searched: the directory of `options.cmakeCache`, the
127
+ * build directories of its CMake presets, and the conventional `build`,
128
+ * `out`, `builddir` and `cmake-build-` directories (see
129
+ * `cmakeBuildDirCandidates`). A database can come with the code it describes,
130
+ * and atom asks the
131
+ * GCC or Clang driver it names (from the PATH, or an absolute path outside
132
+ * the project) for its predefined macros, so in secure mode only an explicit
133
+ * database is used.
134
+ *
135
+ * @param {string} src Project scan root
136
+ * @param {Object} options CLI options
137
+ * @returns {string|undefined} Absolute path of the database
138
+ */
139
+ export function findCompileCommands(src, options = {}) {
140
+ if (options.compileCommands) {
141
+ const requested = resolve(`${options.compileCommands}`);
142
+ let found;
143
+ try {
144
+ found = statSync(requested).isDirectory()
145
+ ? compileCommandsIn(requested)
146
+ : requested;
147
+ } catch (_err) {
148
+ found = undefined;
149
+ }
150
+ if (!found) {
151
+ console.warn(
152
+ `No compilation database at ${requested}. Parsing the C/C++ sources without one.`,
153
+ );
154
+ }
155
+ return found;
156
+ }
157
+ if (isSecureMode || !src) {
158
+ return undefined;
159
+ }
160
+ const root = resolve(src);
161
+ let rootIsDir = false;
162
+ try {
163
+ rootIsDir = statSync(root).isDirectory();
164
+ } catch (_err) {
165
+ return undefined;
166
+ }
167
+ if (!rootIsDir) {
168
+ return undefined;
169
+ }
170
+ const dirs = [root, ...cmakeBuildDirCandidates(root, options)];
171
+ for (const dir of dirs) {
172
+ const candidate = join(dir, COMPILE_COMMANDS_FILE);
173
+ try {
174
+ if (statSync(candidate).isFile()) {
175
+ return candidate;
176
+ }
177
+ } catch (_err) {
178
+ // not there
179
+ }
180
+ }
181
+ return undefined;
182
+ }
183
+
184
+ const atomFrontendArgKeysCache = new Map();
185
+
186
+ /**
187
+ * Split an atom command that may carry a leading argument (`node
188
+ * script.js`, a custom `ATOM_CMD`) into its binary and that argument. The
189
+ * first space only splits when it can: a binary under a path with spaces
190
+ * (`C:\Program Files\...\node.exe script.js`) would otherwise be cut
191
+ * mid-path and the shell told to run `C:\Program`. A command name without a
192
+ * path separator (`node script.js`) resolves through the PATH, so it splits
193
+ * at its first space as before; otherwise the first prefix that exists as a
194
+ * file is the binary, and when none does the first space keeps the old
195
+ * behaviour.
196
+ *
197
+ * @param {string} command The atom command, possibly with an argument
198
+ * @returns {[string, string|undefined]} The binary and its leading argument, when present
199
+ */
200
+ export function splitAtomCommand(command) {
201
+ const firstSpace = `${command}`.indexOf(" ");
202
+ if (firstSpace === -1) {
203
+ return [`${command}`, undefined];
204
+ }
205
+ const head = command.slice(0, firstSpace);
206
+ if (!/[/\\]/.test(head)) {
207
+ return [head, command.slice(firstSpace + 1)];
208
+ }
209
+ let splitAt = firstSpace;
210
+ let index = firstSpace;
211
+ while ((index = command.indexOf(" ", index + 1)) !== -1) {
212
+ try {
213
+ if (statSync(command.slice(0, index)).isFile()) {
214
+ splitAt = index;
215
+ break;
216
+ }
217
+ } catch (_err) {
218
+ // not a file; a later space may still split at the real binary
219
+ }
220
+ }
221
+ return [command.slice(0, splitAt), command.slice(splitAt + 1)];
222
+ }
223
+
224
+ /**
225
+ * The `--frontend-args` keys the installed atom accepts for a language, as
226
+ * `atom --frontend-args-keys -l <language>` lists them. Asked once per atom
227
+ * command and language. An atom that cannot list them (a release without
228
+ * `--frontend-args`, or one that fails to start) accepts none.
229
+ *
230
+ * @param {string} language atom `-l` value
231
+ * @returns {Set<string>} Supported keys
232
+ */
233
+ export function atomFrontendArgKeys(language) {
234
+ const command = getAtomCommand();
235
+ const cacheKey = `${command}\u0000${language}`;
236
+ if (atomFrontendArgKeysCache.has(cacheKey)) {
237
+ return atomFrontendArgKeysCache.get(cacheKey);
238
+ }
239
+ const [bin, leadingArg] = splitAtomCommand(command);
240
+ const args = ["--frontend-args-keys", "-l", `${language}`];
241
+ if (leadingArg !== undefined) {
242
+ args.unshift(leadingArg);
243
+ }
244
+ const env = { ...process.env };
245
+ if (readEnvironmentVariable("ATOM_JAVA_HOME")) {
246
+ env.JAVA_HOME = readEnvironmentVariable("ATOM_JAVA_HOME");
247
+ }
248
+ const keys = new Set();
249
+ const result = safeSpawnSync(bin, args, {
250
+ shell: isWin,
251
+ env,
252
+ timeout: 120000,
253
+ });
254
+ if (result?.status === 0 && result.stdout) {
255
+ let inTable = false;
256
+ for (const line of `${result.stdout}`.split(/\r?\n/)) {
257
+ if (line.startsWith("----")) {
258
+ inTable = true;
259
+ } else if (inTable && /^[a-z][a-z0-9-]*\s/.test(line)) {
260
+ keys.add(line.split(/\s+/)[0]);
261
+ }
262
+ }
263
+ }
264
+ if (DEBUG_MODE && !keys.size) {
265
+ console.log(
266
+ `The installed atom did not list its --frontend-args keys for ${language}.`,
267
+ );
268
+ }
269
+ atomFrontendArgKeysCache.set(cacheKey, keys);
270
+ return keys;
271
+ }
272
+
273
+ /**
274
+ * The `--frontend-args` atom takes for a C/C++ language: the project's
275
+ * compilation database, when there is one and the installed atom reads it
276
+ * (atom 4 and later). An older atom is given the arguments it has always
277
+ * been given, and parses the sources without the database.
278
+ *
279
+ * @param {string} src Project scan root
280
+ * @param {string} language atom `-l` value
281
+ * @param {Object} options CLI options
282
+ * @returns {string[]} Arguments to add to the atom command
283
+ */
284
+ export function atomCompileCommandsArgs(src, language, options = {}) {
285
+ if (!ATOM_C_LANGUAGES.has(`${language}`.toLowerCase())) {
286
+ return [];
287
+ }
288
+ const database = findCompileCommands(src, options);
289
+ if (!database) {
290
+ return [];
291
+ }
292
+ // --frontend-args separates its key=value pairs with commas
293
+ if (database.includes(",")) {
294
+ console.warn(
295
+ `The compilation database path ${database} contains a comma, which atom cannot take. Parsing the C/C++ sources without it.`,
296
+ );
297
+ return [];
298
+ }
299
+ // The argument reaches a Windows shell command line through `shell: isWin`
300
+ // in executeAtom, and the path is routinely discovered inside the scanned
301
+ // tree, so a database whose path a shell would interpret is never passed on.
302
+ if (UNSAFE_SHELL_CHARS_RE.test(database)) {
303
+ console.warn(
304
+ `The compilation database path ${database} contains characters a shell would interpret. Parsing the C/C++ sources without it.`,
305
+ );
306
+ return [];
307
+ }
308
+ if (!atomFrontendArgKeys(language).has("compile-commands")) {
309
+ return [];
310
+ }
311
+ if (DEBUG_MODE) {
312
+ console.log(`Passing the compilation database ${database} to atom.`);
313
+ }
314
+ return ["--frontend-args", `compile-commands=${database}`];
315
+ }
316
+
60
317
  // Absolute ceiling on atom's heap. Past this point a larger heap buys a
61
318
  // collector that runs less often rather than an analysis that succeeds where it
62
319
  // otherwise would not, while the reservation itself is what pushes a build
@@ -489,18 +746,32 @@ export function buildAtomCommandEnv(options = {}, language = "") {
489
746
  // that does not exist on native platforms, and atom 3.0.x crashes parsing
490
747
  // that bogus value. Forward the resolved php-parse location through the env
491
748
  // so executeAtom can bypass the dispatcher for PHP (see
492
- // resolveDirectAtomBinaryPath). Computed independently of exclude patterns.
749
+ // resolveDirectAtomBinaryPath). PHP_ASTGEN_BIN additionally names the
750
+ // batch-capable generator (phpastgen.js): chen parses the whole tree, vendor
751
+ // included, in one generator run instead of one php interpreter per file,
752
+ // which on windows is the difference between minutes and hours. Computed
753
+ // independently of exclude patterns.
493
754
  const phpParseBin = ATOM_PHP_LANGUAGES.has(normalizedLanguage)
494
755
  ? resolvePhpParseBin()
495
756
  : undefined;
757
+ const phpAstgenBin = ATOM_PHP_LANGUAGES.has(normalizedLanguage)
758
+ ? resolvePhpAstgenBin()
759
+ : undefined;
760
+ const phpEnv = {};
761
+ if (phpParseBin) {
762
+ phpEnv.PHP_PARSER_BIN = phpParseBin;
763
+ }
764
+ if (phpAstgenBin) {
765
+ phpEnv.PHP_ASTGEN_BIN = phpAstgenBin;
766
+ }
496
767
  if (!excludePatterns.length) {
497
- return phpParseBin ? { PHP_PARSER_BIN: phpParseBin } : {};
768
+ return phpEnv;
498
769
  }
499
770
  const chenIgnoreDirs = mergeCsvValues(
500
771
  readEnvironmentVariable("CHEN_IGNORE_DIRS"),
501
772
  extractIgnoreDirsFromExcludePatterns(excludePatterns, true),
502
773
  );
503
- const env = {};
774
+ const env = { ...phpEnv };
504
775
  if (chenIgnoreDirs) {
505
776
  env.CHEN_IGNORE_DIRS = chenIgnoreDirs;
506
777
  }
@@ -526,9 +797,6 @@ export function buildAtomCommandEnv(options = {}, language = "") {
526
797
  env.ASTGEN_IGNORE_FILE_PATTERN = astgenIgnoreFilePattern;
527
798
  }
528
799
  }
529
- if (phpParseBin) {
530
- env.PHP_PARSER_BIN = phpParseBin;
531
- }
532
800
  return env;
533
801
  }
534
802
 
@@ -610,12 +878,93 @@ export function resolveAtomProvider(opts = {}) {
610
878
  }
611
879
 
612
880
  /**
613
- * Returns `"native"` or `"jar"` for the currently resolved atom provider.
614
- * Used to gate Java/JDK advice so users on the five native platforms are not
615
- * told to install a JDK for a failure that has nothing to do with Java.
881
+ * Returns `"native"` or `"jar"` for the atom provider that will actually run.
882
+ *
883
+ * The platform decides which provider atom prefers, but the dispatcher falls
884
+ * back to the jar package when the platform's native package is not installed
885
+ * (optional dependencies skipped, a failed download). The two take the heap
886
+ * ceiling differently, and a native-image runtime option on a jar run reaches
887
+ * atom as an unknown argument that fails every slice. So the kind follows what
888
+ * is installed where cdxgen can see it: the native binary means native, the jar
889
+ * package without it means jar. An `ATOM_CMD` is classified by what it names
890
+ * (see `atomCommandKind`). With nothing in view (an install elsewhere) the
891
+ * platform's kind stands.
892
+ *
893
+ * Also used to gate Java/JDK advice, so users on the five native platforms are
894
+ * not told to install a JDK for a failure that has nothing to do with Java.
616
895
  */
617
896
  export function atomProviderKind() {
618
- return resolveAtomProvider().kind;
897
+ const { kind } = resolveAtomProvider();
898
+ if (readEnvironmentVariable("ATOM_CMD")) {
899
+ return atomCommandKind(readEnvironmentVariable("ATOM_CMD"), kind);
900
+ }
901
+ if (kind !== "native") {
902
+ return kind;
903
+ }
904
+ const atomHome = readEnvironmentVariable("ATOM_HOME");
905
+ if (atomHome) {
906
+ // A jar install's ATOM_HOME is its plugins/ directory, which carries lib/;
907
+ // a native sub-package directory does not.
908
+ return safeExistsSync(join(atomHome, "lib")) ? "jar" : kind;
909
+ }
910
+ if (resolveDirectAtomBinaryPath()) {
911
+ return "native";
912
+ }
913
+ const jarPlugins = atomSubPackageCandidates("atom-jar").some((dir) =>
914
+ safeExistsSync(join(dir, "plugins")),
915
+ );
916
+ return jarPlugins ? "jar" : kind;
917
+ }
918
+
919
+ /**
920
+ * Whether `ATOM_CMD` launches a jar or follows the platform.
921
+ *
922
+ * The jar launchers (`plugins/bin/atom`, `plugins/bin/atom.bat`) sit next to a
923
+ * `plugins/lib` directory holding atom's jars, so a launcher with that sibling,
924
+ * or any `.bat` (only the jar payload ships one), runs on the JVM whatever the
925
+ * platform is. Everything else, including the npm dispatcher (`node
926
+ * .../index.js`, its `atom`/`atom.cmd` bin shims) and a native binary, follows
927
+ * the platform's kind. A GraalVM `-XX:` heap argument handed to a jar launcher
928
+ * is rejected by atom's option parser before any analysis starts, and a
929
+ * `JAVA_TOOL_OPTIONS` ceiling handed to a native image is silently ignored, so
930
+ * the distinction decides whether the heap is bounded at all.
931
+ *
932
+ * @param {string} command The `ATOM_CMD` value
933
+ * @param {"native"|"jar"} platformKind Kind the platform prefers
934
+ * @returns {"native"|"jar"}
935
+ */
936
+ export function atomCommandKind(command, platformKind) {
937
+ const [bin, leadingArg] = splitAtomCommand(command);
938
+ const runsScript =
939
+ leadingArg !== undefined && /^node(\.exe)?$/i.test(basename(bin));
940
+ let launcher = runsScript ? leadingArg : bin;
941
+ if (/\.bat$/i.test(launcher)) {
942
+ return "jar";
943
+ }
944
+ try {
945
+ // npm bin entries are symlinks into the package (`/usr/local/bin/atom` ->
946
+ // `.../@appthreat/atom/index.js`), and `/usr/local` has a `lib` of its own.
947
+ launcher = realpathSync(launcher);
948
+ } catch {
949
+ return platformKind;
950
+ }
951
+ const binDir = dirname(launcher);
952
+ if (basename(binDir) !== "bin") {
953
+ return platformKind;
954
+ }
955
+ try {
956
+ const libDir = join(dirname(binDir), "lib");
957
+ if (
958
+ readdirSync(libDir).some(
959
+ (file) => file.startsWith("io.appthreat.atom") && file.endsWith(".jar"),
960
+ )
961
+ ) {
962
+ return "jar";
963
+ }
964
+ } catch {
965
+ // no lib directory: not a jar layout
966
+ }
967
+ return platformKind;
619
968
  }
620
969
 
621
970
  /**
@@ -623,8 +972,8 @@ export function atomProviderKind() {
623
972
  *
624
973
  * atom 3's dispatcher unconditionally sets `PHP_PARSER_BIN=<ATOM_HOME>/bin/php-parse`,
625
974
  * which for a native sub-package does not exist and also clobbers a caller-set
626
- * value. cdxgen therefore resolves the real location and forwards it through
627
- * the child env (see `buildAtomCommandEnv`); `executeAtom` then spawns the
975
+ * value. cdxgen therefore resolves the real location and forwards it through the
976
+ * child env (see `buildAtomCommandEnv`); `executeAtom` then spawns the
628
977
  * native binary directly for PHP so the dispatcher cannot clobber it (see
629
978
  * `resolveDirectAtomBinaryPath`). Resolution order:
630
979
  * 1. explicit `PHP_PARSER_BIN` env var (operators / container images)
@@ -641,6 +990,44 @@ export function resolvePhpParseBin() {
641
990
  if (readEnvironmentVariable("PHP_PARSER_BIN")) {
642
991
  return readEnvironmentVariable("PHP_PARSER_BIN");
643
992
  }
993
+ return parsetoolsFile("plugins", "bin", "php-parse");
994
+ }
995
+
996
+ /**
997
+ * Locate the `phpastgen` generator that lets the PHP frontend parse whole
998
+ * directories in one batch.
999
+ *
1000
+ * `php-parse` (forwarded as `PHP_PARSER_BIN`) is the per-file parser: atom
1001
+ * starts a php interpreter for every `.php` file, which on windows turns a
1002
+ * vendored PHP project (thousands of files under `vendor/`) into an hours-long
1003
+ * run that no timeout can rescue. `phpastgen` answers the `--parser-info`
1004
+ * capability probe, so atom's batch path engages, and atom-parsetools 1.9 and
1005
+ * later parse the whole tree, vendor included, with a few interpreters that
1006
+ * each take a chunk of files. It is a Node script, so the frontend launches it
1007
+ * with `node` rather than `php`; an atom that does not know `PHP_ASTGEN_BIN`
1008
+ * simply ignores it and keeps today's per-file behaviour. Resolution order:
1009
+ * 1. explicit `PHP_ASTGEN_BIN` env var (operators / container images)
1010
+ * 2. `@appthreat/atom-parsetools/phpastgen.js` under cdxgen's own
1011
+ * node_modules, then under `GLOBAL_NODE_MODULES_PATH` for global installs
1012
+ *
1013
+ * @returns {string|undefined}
1014
+ */
1015
+ export function resolvePhpAstgenBin() {
1016
+ if (readEnvironmentVariable("PHP_ASTGEN_BIN")) {
1017
+ return readEnvironmentVariable("PHP_ASTGEN_BIN");
1018
+ }
1019
+ return parsetoolsFile("phpastgen.js");
1020
+ }
1021
+
1022
+ /**
1023
+ * Resolve a file inside the installed `@appthreat/atom-parsetools` package,
1024
+ * searching cdxgen's own node_modules and `GLOBAL_NODE_MODULES_PATH` for global
1025
+ * installs.
1026
+ *
1027
+ * @param {...string} segments Path segments below the package root
1028
+ * @returns {string|undefined}
1029
+ */
1030
+ export function parsetoolsFile(...segments) {
644
1031
  const roots = [dirNameStr];
645
1032
  if (readEnvironmentVariable("GLOBAL_NODE_MODULES_PATH")) {
646
1033
  roots.push(dirname(readEnvironmentVariable("GLOBAL_NODE_MODULES_PATH")));
@@ -651,9 +1038,7 @@ export function resolvePhpParseBin() {
651
1038
  "node_modules",
652
1039
  "@appthreat",
653
1040
  "atom-parsetools",
654
- "plugins",
655
- "bin",
656
- "php-parse",
1041
+ ...segments,
657
1042
  );
658
1043
  if (safeExistsSync(candidate)) {
659
1044
  return candidate;
@@ -683,6 +1068,25 @@ export function resolveDirectAtomBinaryPath() {
683
1068
  }
684
1069
  const folder = preferredPkg.split("/")[1];
685
1070
  const exeName = isWin ? "atom.exe" : "atom";
1071
+ for (const dir of atomSubPackageCandidates(folder)) {
1072
+ const candidate = join(dir, "bin", exeName);
1073
+ if (safeExistsSync(candidate)) {
1074
+ return candidate;
1075
+ }
1076
+ }
1077
+ return undefined;
1078
+ }
1079
+
1080
+ /**
1081
+ * Directories where an `@appthreat/atom-*` sub-package may be installed next to
1082
+ * the dispatcher cdxgen uses, for npm, pnpm and global layouts: a sibling of
1083
+ * `@appthreat/atom`, nested under it, in a pnpm virtual store, or a sibling of
1084
+ * the dispatcher's real (symlink-resolved) directory.
1085
+ *
1086
+ * @param {string} folder Sub-package folder name, e.g. `atom-jar`
1087
+ * @returns {string[]} Candidate package directories
1088
+ */
1089
+ function atomSubPackageCandidates(folder) {
686
1090
  const version = readAtomVersion();
687
1091
  const roots = [join(dirNameStr, "node_modules")];
688
1092
  if (readEnvironmentVariable("GLOBAL_NODE_MODULES_PATH")) {
@@ -690,18 +1094,9 @@ export function resolveDirectAtomBinaryPath() {
690
1094
  }
691
1095
  const candidates = [];
692
1096
  for (const root of roots) {
693
- candidates.push(join(root, "@appthreat", folder, "bin", exeName));
1097
+ candidates.push(join(root, "@appthreat", folder));
694
1098
  candidates.push(
695
- join(
696
- root,
697
- "@appthreat",
698
- "atom",
699
- "node_modules",
700
- "@appthreat",
701
- folder,
702
- "bin",
703
- exeName,
704
- ),
1099
+ join(root, "@appthreat", "atom", "node_modules", "@appthreat", folder),
705
1100
  );
706
1101
  if (version) {
707
1102
  candidates.push(
@@ -712,18 +1107,19 @@ export function resolveDirectAtomBinaryPath() {
712
1107
  "node_modules",
713
1108
  "@appthreat",
714
1109
  folder,
715
- "bin",
716
- exeName,
717
1110
  ),
718
1111
  );
719
1112
  }
720
- }
721
- for (const candidate of candidates) {
722
- if (safeExistsSync(candidate)) {
723
- return candidate;
1113
+ // pnpm links @appthreat/atom to its store entry, where the sub-packages
1114
+ // are its siblings.
1115
+ try {
1116
+ const atomDir = realpathSync(join(root, "@appthreat", "atom"));
1117
+ candidates.push(join(dirname(atomDir), folder));
1118
+ } catch {
1119
+ // not installed under this root
724
1120
  }
725
1121
  }
726
- return undefined;
1122
+ return [...new Set(candidates)];
727
1123
  }
728
1124
 
729
1125
  function readAtomVersion() {
@@ -874,6 +1270,117 @@ function applyAtomHeapLimit(args, env) {
874
1270
  }
875
1271
  }
876
1272
 
1273
+ // How long cdxgen waits past atom's own time limit for the dispatcher to stop
1274
+ // the runtime and exit. It covers the dispatcher's SIGTERM-to-SIGKILL grace (10
1275
+ // seconds) and the runtime's exit. A short limit (a test, a smoke run) gets a
1276
+ // quarter of itself instead.
1277
+ const ATOM_STOP_GRACE_MS = 30_000;
1278
+
1279
+ /**
1280
+ * Compute how long atom may run, and how long cdxgen waits for it.
1281
+ *
1282
+ * cdxgen's spawn timeout kills only the process cdxgen started, and for atom
1283
+ * that is the npm dispatcher. The runtime under it (the native binary or the
1284
+ * JVM) used to survive that kill, re-parented and still holding its heap. atom
1285
+ * therefore gets its own limit, `ATOM_TIMEOUT`, which the dispatcher enforces on
1286
+ * the runtime, and cdxgen's spawn timeout sits a grace period beyond it as the
1287
+ * last resort.
1288
+ *
1289
+ * An explicit `ATOM_TIMEOUT` (milliseconds) sets atom's limit independently of
1290
+ * `CDXGEN_TIMEOUT_MS`, which keeps bounding every other tool. Otherwise atom's
1291
+ * limit is `CDXGEN_TIMEOUT_MS` less the grace period.
1292
+ *
1293
+ * @returns {{atomTimeoutMs: number, spawnTimeoutMs: number}} atom's own limit and cdxgen's spawn timeout for it
1294
+ */
1295
+ export function atomTimeouts() {
1296
+ const graceFor = (ms) => Math.min(ATOM_STOP_GRACE_MS, Math.floor(ms / 4));
1297
+ const configured = Number.parseInt(
1298
+ readEnvironmentVariable("ATOM_TIMEOUT"),
1299
+ 10,
1300
+ );
1301
+ if (configured > 0) {
1302
+ return {
1303
+ atomTimeoutMs: configured,
1304
+ spawnTimeoutMs: configured + graceFor(configured),
1305
+ };
1306
+ }
1307
+ return {
1308
+ atomTimeoutMs: TIMEOUT_MS - graceFor(TIMEOUT_MS),
1309
+ spawnTimeoutMs: TIMEOUT_MS,
1310
+ };
1311
+ }
1312
+
1313
+ // The status the atom dispatcher exits with when ATOM_TIMEOUT stops atom.
1314
+ const ATOM_TIMEOUT_EXIT_CODE = 124;
1315
+
1316
+ /**
1317
+ * Reap everything a timed-out atom run left behind on Windows.
1318
+ *
1319
+ * cdxgen starts atom through a shell there (`shell: isWin`), so the spawn
1320
+ * timeout terminates only that `cmd.exe`, and the runtime and every parser
1321
+ * worker under it (one `php.exe` per file for the per-file PHP frontend) keep
1322
+ * running against the next slice of the same run. See
1323
+ * {@link reapProcessTree} for how they are found. A chain broken by an
1324
+ * intermediate that already exited (the npm dispatcher stopping its runtime
1325
+ * itself) cannot be followed, and atom 4 stops its helpers once
1326
+ * `ATOM_PARENT_PID` is gone.
1327
+ *
1328
+ * @param {number|undefined} rootPid pid of the process cdxgen spawned
1329
+ * @param {number} startedAt Epoch milliseconds at which the run started
1330
+ * @param {number} [endedAt] Epoch milliseconds at which the spawned process ended
1331
+ * @returns {number[]} The pids that were stopped
1332
+ */
1333
+ export function reapAtomProcessTree(rootPid, startedAt, endedAt = Date.now()) {
1334
+ return reapProcessTree(rootPid, startedAt, endedAt, "atom");
1335
+ }
1336
+
1337
+ /**
1338
+ * Whether an atom run ended because it ran out of time.
1339
+ *
1340
+ * The dispatcher reports its own limit with status 124. cdxgen's spawn timeout
1341
+ * shows up as `ETIMEDOUT`. A dispatcher older than the status code stops atom
1342
+ * with SIGTERM and passes on how the runtime ended: 1, or the runtime's own
1343
+ * exit on SIGTERM (143) or SIGKILL (137), which is what atom 3.1 reports. Those
1344
+ * statuses at or past the limit count as well, unless atom's own output shows
1345
+ * it failed for another reason first (an exhausted heap, a crash), which
1346
+ * deserves its own diagnosis. Missing one leaves the half-written atom behind,
1347
+ * and the next slice of the run fails to load it.
1348
+ *
1349
+ * @param {Object} result spawnSync result
1350
+ * @param {number} elapsedMs How long the run took
1351
+ * @param {number} atomTimeoutMs atom's time limit
1352
+ * @returns {boolean} true when the run hit a time limit
1353
+ */
1354
+ export function atomRunTimedOut(result, elapsedMs, atomTimeoutMs) {
1355
+ if (result?.status === ATOM_TIMEOUT_EXIT_CODE) {
1356
+ return true;
1357
+ }
1358
+ if (result?.error?.code === "ETIMEDOUT") {
1359
+ return true;
1360
+ }
1361
+ if (
1362
+ !ATOM_STOPPED_STATUSES.includes(result?.status) ||
1363
+ elapsedMs < atomTimeoutMs
1364
+ ) {
1365
+ return false;
1366
+ }
1367
+ const output = `${result.stdout || ""}\n${result.stderr || ""}`;
1368
+ return !ATOM_FAILURE_MARKERS.some((marker) => output.includes(marker));
1369
+ }
1370
+
1371
+ // How a runtime stopped by an older dispatcher at ATOM_TIMEOUT exits: the
1372
+ // dispatcher's own 1, or the runtime's exit on SIGTERM (128 + 15) or SIGKILL
1373
+ // (128 + 9) passed through.
1374
+ const ATOM_STOPPED_STATUSES = [1, 137, 143];
1375
+
1376
+ // Output that means atom failed by itself, whatever the clock says.
1377
+ const ATOM_FAILURE_MARKERS = [
1378
+ "OutOfMemoryError",
1379
+ "A fatal error has been detected by the Java Runtime Environment",
1380
+ "The crash happened outside the Java Virtual Machine in native code",
1381
+ "Failure: ",
1382
+ ];
1383
+
877
1384
  /**
878
1385
  * Execute the atom tool against a source directory or file with the given arguments.
879
1386
  *
@@ -914,14 +1421,12 @@ export function executeAtom(src, args, extra_env = {}) {
914
1421
  ...extra_env,
915
1422
  };
916
1423
  // Bound the heap while argv still starts at atom's own arguments: the
917
- // space-split below prepends the launcher script, which has to stay first.
1424
+ // split below prepends the launcher script, which has to stay first.
918
1425
  applyAtomHeapLimit(args, env);
919
- if (ATOM_BIN.includes(" ")) {
920
- const tmpA = ATOM_BIN.split(" ");
921
- if (tmpA && tmpA.length > 1) {
922
- ATOM_BIN = tmpA[0];
923
- args.unshift(tmpA[1]);
924
- }
1426
+ const [resolvedBin, leadingArg] = splitAtomCommand(ATOM_BIN);
1427
+ if (leadingArg !== undefined) {
1428
+ ATOM_BIN = resolvedBin;
1429
+ args.unshift(leadingArg);
925
1430
  }
926
1431
  if (DEBUG_MODE) {
927
1432
  console.log("Executing", ATOM_BIN);
@@ -952,12 +1457,35 @@ export function executeAtom(src, args, extra_env = {}) {
952
1457
  ".bin",
953
1458
  )}`;
954
1459
  }
1460
+ const { atomTimeoutMs, spawnTimeoutMs } = atomTimeouts();
1461
+ env.ATOM_TIMEOUT = String(atomTimeoutMs);
1462
+ // Name cdxgen as the supervisor: the dispatcher, and atom under it, stop when
1463
+ // cdxgen is gone (killed by a caller's own timeout, say) instead of analysing
1464
+ // on for nobody.
1465
+ env.ATOM_PARENT_PID = String(process.pid);
1466
+ const startedAt = Date.now();
955
1467
  const result = safeSpawnSync(ATOM_BIN, args, {
956
1468
  cwd,
957
1469
  shell: isWin,
958
1470
  killSignal: "SIGKILL",
959
1471
  env,
1472
+ timeout: spawnTimeoutMs,
960
1473
  });
1474
+ const endedAt = Date.now();
1475
+ if (atomRunTimedOut(result, endedAt - startedAt, atomTimeoutMs)) {
1476
+ console.warn(
1477
+ `WARN: atom did not finish within ${formatDuration(atomTimeoutMs)} and was stopped, so its results are missing or incomplete. Allow more time with ATOM_TIMEOUT (milliseconds, atom only) or CDXGEN_TIMEOUT_MS (every external command).`,
1478
+ );
1479
+ reapAtomProcessTree(result.pid, startedAt, endedAt);
1480
+ // A stopped atom leaves its graph half written, and the next slice of the
1481
+ // same run would try to reuse it and fail to load it.
1482
+ const atomFileIndex = args.indexOf("-o");
1483
+ const atomFile = atomFileIndex >= 0 ? args[atomFileIndex + 1] : undefined;
1484
+ if (atomFile && safeExistsSync(atomFile)) {
1485
+ safeRmSync(atomFile, { recursive: true, force: true });
1486
+ }
1487
+ return false;
1488
+ }
961
1489
  const isJarKind = atomProviderKind() === "jar";
962
1490
  if (result.stderr) {
963
1491
  if (
@@ -1036,6 +1564,130 @@ export function executeAtom(src, args, extra_env = {}) {
1036
1564
  return isSupported && !result.error && result.status === 0;
1037
1565
  }
1038
1566
 
1567
+ const CHUNK_MTIME_SLACK_MS = 2000;
1568
+
1569
+ /**
1570
+ * The numbered chunks atom wrote beside a reachables slices file.
1571
+ *
1572
+ * atom writes reachables at most 1000 flows per file: `<base>.json`, then
1573
+ * `<base>_1.json`, `<base>_2.json` and so on. Chunks are taken until the first
1574
+ * gap. A chunk older than the base file was left by an earlier run with more
1575
+ * flows (atom before 4.0.0 did not remove those), so it ends the sequence
1576
+ * instead of being merged.
1577
+ *
1578
+ * @param {string} slicesFile Path of the base reachables slices file
1579
+ * @returns {string[]} Chunk file paths, in order
1580
+ */
1581
+ export function reachablesChunkFiles(slicesFile) {
1582
+ const baseMtimeMs = mtimeMsOf(slicesFile);
1583
+ if (!slicesFile?.endsWith(".json") || baseMtimeMs === undefined) {
1584
+ return [];
1585
+ }
1586
+ const stem = slicesFile.slice(0, -".json".length);
1587
+ // atom writes the base file first, so its chunks are never older. The slack
1588
+ // covers filesystems that store modification times in whole seconds or two;
1589
+ // it assumes no earlier run wrote chunks to the same path within that window.
1590
+ // atom 4 and cdxgen both remove old chunks before atom writes new ones, so
1591
+ // the mtime test only matters for slices files produced elsewhere.
1592
+ const oldestChunkMtimeMs = baseMtimeMs - CHUNK_MTIME_SLACK_MS;
1593
+ const chunks = [];
1594
+ for (let index = 1; ; index++) {
1595
+ const chunk = `${stem}_${index}.json`;
1596
+ const chunkMtimeMs = mtimeMsOf(chunk);
1597
+ if (chunkMtimeMs === undefined || chunkMtimeMs < oldestChunkMtimeMs) {
1598
+ break;
1599
+ }
1600
+ chunks.push(chunk);
1601
+ }
1602
+ return chunks;
1603
+ }
1604
+
1605
+ // A file that disappears between listing and stat ends the sequence, not the run.
1606
+ function mtimeMsOf(file) {
1607
+ if (!file || !safeExistsSync(file)) {
1608
+ return undefined;
1609
+ }
1610
+ try {
1611
+ return statSync(file).mtimeMs;
1612
+ } catch {
1613
+ return undefined;
1614
+ }
1615
+ }
1616
+
1617
+ /**
1618
+ * Remove the numbered chunks beside a reachables slices file, before atom
1619
+ * writes a new set there, so no chunk of an earlier run survives next to it.
1620
+ *
1621
+ * @param {string} slicesFile Path of the base reachables slices file
1622
+ */
1623
+ export function removeReachablesChunkFiles(slicesFile) {
1624
+ if (!slicesFile?.endsWith(".json")) {
1625
+ return;
1626
+ }
1627
+ const stem = slicesFile.slice(0, -".json".length);
1628
+ for (let index = 1; safeExistsSync(`${stem}_${index}.json`); index++) {
1629
+ safeRmSync(`${stem}_${index}.json`, { force: true });
1630
+ }
1631
+ }
1632
+
1633
+ /**
1634
+ * Read an atom slices file, or warn and return `undefined` when it cannot be
1635
+ * parsed. A run stopped while writing (a timeout, a kill) can leave a
1636
+ * truncated file, and the evidence from the other slices is still worth
1637
+ * keeping.
1638
+ *
1639
+ * @param {string} slicesFile Path of the slices file
1640
+ * @param {string} sliceType Slice type, for the warning
1641
+ * @returns {*} The parsed slices, or `undefined`
1642
+ */
1643
+ export function readSlicesFile(slicesFile, sliceType) {
1644
+ if (!slicesFile || !safeExistsSync(slicesFile)) {
1645
+ return undefined;
1646
+ }
1647
+ try {
1648
+ return readJsonFile(slicesFile);
1649
+ } catch (e) {
1650
+ console.warn(
1651
+ `WARN: Ignoring the ${sliceType} slices file ${slicesFile}, which is incomplete or not valid JSON (${e.message}).`,
1652
+ );
1653
+ return undefined;
1654
+ }
1655
+ }
1656
+
1657
+ /**
1658
+ * Read a reachables slices file together with its chunks.
1659
+ *
1660
+ * Reading only the base file silently dropped every flow past the first
1661
+ * thousand. A chunk that cannot be parsed ends the sequence, keeping the flows
1662
+ * read before it.
1663
+ *
1664
+ * @param {string} slicesFile Path of the base reachables slices file
1665
+ * @returns {Object[]|Object|undefined} Every flow, in the base file's shape (an array, or an object with `reachables`)
1666
+ */
1667
+ export function readReachablesSlices(slicesFile) {
1668
+ const data = readSlicesFile(slicesFile, "reachables");
1669
+ if (data === undefined) {
1670
+ return undefined;
1671
+ }
1672
+ const chunks = reachablesChunkFiles(slicesFile);
1673
+ if (!chunks.length) {
1674
+ return data;
1675
+ }
1676
+ const flowsOf = (slices) =>
1677
+ Array.isArray(slices) ? slices : slices?.reachables || [];
1678
+ const flows = [...flowsOf(data)];
1679
+ for (const chunk of chunks) {
1680
+ const chunkData = readSlicesFile(chunk, "reachables");
1681
+ if (chunkData === undefined) {
1682
+ break;
1683
+ }
1684
+ for (const flow of flowsOf(chunkData)) {
1685
+ flows.push(flow);
1686
+ }
1687
+ }
1688
+ return Array.isArray(data) || !data ? flows : { ...data, reachables: flows };
1689
+ }
1690
+
1039
1691
  /**
1040
1692
  * Find the imported modules in the application with atom parsedeps command
1041
1693
  *
@@ -1068,6 +1720,14 @@ export function findAppModules(
1068
1720
  "--slice-outfile",
1069
1721
  resolve(slicesFile),
1070
1722
  ];
1723
+ // The header languages parse without function bodies. Their AST cache, kept
1724
+ // in <src>/.chen, would be replayed by a later full C/C++ run over the same
1725
+ // directory (evinse, or `-t c --deep`) on atom releases that do not key the
1726
+ // cache on that difference, leaving that run with no method bodies.
1727
+ if (ATOM_HEADER_LANGUAGES.has(`${language}`.toLowerCase())) {
1728
+ args.push("--no-ast-cache");
1729
+ }
1730
+ args.push(...atomCompileCommandsArgs(src, language, options));
1071
1731
  args.push(resolve(src));
1072
1732
  executeAtom(src, args, buildAtomCommandEnv(options, language));
1073
1733
  if (safeExistsSync(slicesFile)) {