@cdxgen/cdxgen 13.1.0 → 13.3.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 (283) hide show
  1. package/README.md +32 -15
  2. package/bin/cdxgen.js +243 -122
  3. package/bin/evinse.js +7 -0
  4. package/bin/sign.js +88 -14
  5. package/bin/tracebom.js +64 -4
  6. package/bin/validate.js +29 -7
  7. package/bin/verify.js +79 -39
  8. package/data/crypto-oid.json +88 -12
  9. package/data/remediations.json +47 -0
  10. package/data/rules/ai-bom.yaml +78 -0
  11. package/data/rules/cargo-rust.yaml +132 -0
  12. package/data/rules/mcp-servers.yaml +122 -0
  13. package/lib/audit/targets.js +6 -1
  14. package/lib/cli/bomAssembly.js +117 -15
  15. package/lib/cli/cliOptions.js +93 -1
  16. package/lib/cli/index.js +235 -5
  17. package/lib/cli/jsBom.js +108 -26
  18. package/lib/cli/jvmBom.js +68 -12
  19. package/lib/cli/managedBom.js +264 -41
  20. package/lib/cli/nativeBom.js +923 -15
  21. package/lib/core/activity.js +120 -2
  22. package/lib/core/env.js +44 -7
  23. package/lib/core/fs.js +174 -6
  24. package/lib/core/metadataProperties.js +97 -0
  25. package/lib/core/projectConfig.js +31 -4
  26. package/lib/core/userAgent.js +151 -0
  27. package/lib/ecosystems/bunutils.js +269 -101
  28. package/lib/ecosystems/cargoCache.js +364 -0
  29. package/lib/ecosystems/caxa.js +447 -7
  30. package/lib/ecosystems/cmakeResolver.js +66 -0
  31. package/lib/ecosystems/cppEvidence.js +104 -33
  32. package/lib/ecosystems/ecosystems.js +260 -99
  33. package/lib/ecosystems/golem.js +11 -1
  34. package/lib/ecosystems/gradleutils.js +185 -9
  35. package/lib/ecosystems/jsEvidence.js +19 -20
  36. package/lib/ecosystems/kosi.js +1263 -0
  37. package/lib/ecosystems/npmutils.js +110 -9
  38. package/lib/ecosystems/parsers-crystal.js +179 -0
  39. package/lib/ecosystems/parsers-dotnet.js +120 -49
  40. package/lib/ecosystems/parsers-elm.js +816 -0
  41. package/lib/ecosystems/parsers-gleam.js +1 -1
  42. package/lib/ecosystems/parsers-go.js +47 -9
  43. package/lib/ecosystems/parsers-js.js +171 -50
  44. package/lib/ecosystems/parsers-julia.js +268 -0
  45. package/lib/ecosystems/parsers-lua.js +218 -0
  46. package/lib/ecosystems/parsers-misc.js +919 -161
  47. package/lib/ecosystems/parsers-nim.js +221 -0
  48. package/lib/ecosystems/parsers-opam.js +180 -0
  49. package/lib/ecosystems/parsers-perl.js +277 -0
  50. package/lib/ecosystems/parsers-python.js +27 -15
  51. package/lib/ecosystems/parsers-r.js +148 -0
  52. package/lib/ecosystems/parsers-rebar.js +411 -0
  53. package/lib/ecosystems/parsers-rust.js +954 -33
  54. package/lib/ecosystems/parsers-spack.js +204 -0
  55. package/lib/ecosystems/parsers-stack.js +264 -0
  56. package/lib/ecosystems/parsers-terraform.js +194 -0
  57. package/lib/ecosystems/rubyutils.js +1426 -166
  58. package/lib/ecosystems/rusi.js +158 -7
  59. package/lib/evinser/evinser.js +547 -129
  60. package/lib/evinser/swiftsem.js +1914 -302
  61. package/lib/helpers/agenticToolutils.js +925 -0
  62. package/lib/helpers/bomSigner.js +652 -221
  63. package/lib/inventory/agentFormulationParser.js +57 -4
  64. package/lib/inventory/aiCollector.js +504 -10
  65. package/lib/inventory/aiInventory.js +12 -0
  66. package/lib/inventory/analyzer.js +488 -30
  67. package/lib/inventory/astWorker.js +6 -0
  68. package/lib/inventory/atomUtils.js +568 -31
  69. package/lib/inventory/auditCategories.js +8 -0
  70. package/lib/inventory/cbomutils.js +30 -0
  71. package/lib/inventory/cdxrs.js +110 -14
  72. package/lib/inventory/communityAiConfigParser.js +215 -2
  73. package/lib/inventory/depsUtils.js +183 -11
  74. package/lib/inventory/display.js +4 -5
  75. package/lib/inventory/dosai.js +627 -13
  76. package/lib/inventory/dynamic.js +39 -1
  77. package/lib/inventory/envcontext.js +9 -6
  78. package/lib/inventory/evidenceUtils.js +26 -5
  79. package/lib/inventory/fetchBatch.js +190 -19
  80. package/lib/inventory/fetchRate.js +74 -11
  81. package/lib/inventory/forgeEnricher.js +3 -2
  82. package/lib/inventory/jvmToolEnv.js +6 -5
  83. package/lib/inventory/mcpConfigParser.js +166 -2
  84. package/lib/inventory/mcpDiscovery.js +11 -0
  85. package/lib/inventory/mcpServerManifest.js +389 -0
  86. package/lib/inventory/mcpbBundle.js +419 -0
  87. package/lib/inventory/plugins.js +3 -0
  88. package/lib/inventory/protobom.js +62 -89
  89. package/lib/inventory/protobomLoader.js +4 -0
  90. package/lib/inventory/purl.js +152 -3
  91. package/lib/inventory/rustFormulationParser.js +1 -1
  92. package/lib/inventory/sbomDocument.js +2 -70
  93. package/lib/inventory/source.js +47 -23
  94. package/lib/inventory/stylePackageRefs.js +41 -0
  95. package/lib/inventory/svelteUtils.js +524 -0
  96. package/lib/inventory/traceRunner.js +210 -21
  97. package/lib/managers/binary.js +115 -24
  98. package/lib/managers/docker.js +440 -20
  99. package/lib/managers/oci.js +39 -1
  100. package/lib/parsers/cmakeLists.js +452 -0
  101. package/lib/parsers/huggingfaceUtils.js +15 -7
  102. package/lib/parsers/largeJson.js +454 -0
  103. package/lib/parsers/npmrc.js +19 -1
  104. package/lib/parsers/onnx.js +294 -0
  105. package/lib/parsers/safetensors.js +114 -0
  106. package/lib/stages/postgen/introspection/markers.js +29 -6
  107. package/lib/stages/postgen/postgen.js +237 -16
  108. package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +462 -0
  109. package/lib/validator/bomValidator.js +6 -16
  110. package/lib/validator/index.js +48 -10
  111. package/lib/validator/reporters/console.js +3 -0
  112. package/lib/validator/reporters/json.js +1 -0
  113. package/lib/validator/reporters/sarif.js +7 -0
  114. package/package.json +27 -27
  115. package/types/lib/audit/targets.d.ts.map +1 -1
  116. package/types/lib/cli/bomAssembly.d.ts +2 -1
  117. package/types/lib/cli/bomAssembly.d.ts.map +1 -1
  118. package/types/lib/cli/cliOptions.d.ts +33 -1
  119. package/types/lib/cli/cliOptions.d.ts.map +1 -1
  120. package/types/lib/cli/index.d.ts +2 -2
  121. package/types/lib/cli/index.d.ts.map +1 -1
  122. package/types/lib/cli/jsBom.d.ts +23 -0
  123. package/types/lib/cli/jsBom.d.ts.map +1 -1
  124. package/types/lib/cli/jvmBom.d.ts.map +1 -1
  125. package/types/lib/cli/managedBom.d.ts +33 -0
  126. package/types/lib/cli/managedBom.d.ts.map +1 -1
  127. package/types/lib/cli/nativeBom.d.ts +88 -8
  128. package/types/lib/cli/nativeBom.d.ts.map +1 -1
  129. package/types/lib/core/activity.d.ts.map +1 -1
  130. package/types/lib/core/env.d.ts +21 -0
  131. package/types/lib/core/env.d.ts.map +1 -1
  132. package/types/lib/core/fs.d.ts +55 -0
  133. package/types/lib/core/fs.d.ts.map +1 -1
  134. package/types/lib/core/metadataProperties.d.ts +39 -0
  135. package/types/lib/core/metadataProperties.d.ts.map +1 -0
  136. package/types/lib/core/projectConfig.d.ts +6 -2
  137. package/types/lib/core/projectConfig.d.ts.map +1 -1
  138. package/types/lib/core/userAgent.d.ts +56 -0
  139. package/types/lib/core/userAgent.d.ts.map +1 -0
  140. package/types/lib/ecosystems/bunutils.d.ts +12 -5
  141. package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
  142. package/types/lib/ecosystems/cargoCache.d.ts +75 -0
  143. package/types/lib/ecosystems/cargoCache.d.ts.map +1 -0
  144. package/types/lib/ecosystems/caxa.d.ts +14 -0
  145. package/types/lib/ecosystems/caxa.d.ts.map +1 -1
  146. package/types/lib/ecosystems/cmakeResolver.d.ts +23 -0
  147. package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
  148. package/types/lib/ecosystems/cppEvidence.d.ts +2 -2
  149. package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
  150. package/types/lib/ecosystems/ecosystems.d.ts +15 -1
  151. package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
  152. package/types/lib/ecosystems/golem.d.ts.map +1 -1
  153. package/types/lib/ecosystems/gradleutils.d.ts +40 -4
  154. package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
  155. package/types/lib/ecosystems/jsEvidence.d.ts +5 -2
  156. package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
  157. package/types/lib/ecosystems/kosi.d.ts +86 -0
  158. package/types/lib/ecosystems/kosi.d.ts.map +1 -0
  159. package/types/lib/ecosystems/npmutils.d.ts +42 -0
  160. package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
  161. package/types/lib/ecosystems/parsers-crystal.d.ts +27 -0
  162. package/types/lib/ecosystems/parsers-crystal.d.ts.map +1 -0
  163. package/types/lib/ecosystems/parsers-dotnet.d.ts +19 -0
  164. package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
  165. package/types/lib/ecosystems/parsers-elm.d.ts +79 -0
  166. package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -0
  167. package/types/lib/ecosystems/parsers-go.d.ts +10 -0
  168. package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
  169. package/types/lib/ecosystems/parsers-js.d.ts +10 -4
  170. package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
  171. package/types/lib/ecosystems/parsers-julia.d.ts +57 -0
  172. package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -0
  173. package/types/lib/ecosystems/parsers-lua.d.ts +50 -0
  174. package/types/lib/ecosystems/parsers-lua.d.ts.map +1 -0
  175. package/types/lib/ecosystems/parsers-misc.d.ts +90 -3
  176. package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
  177. package/types/lib/ecosystems/parsers-nim.d.ts +43 -0
  178. package/types/lib/ecosystems/parsers-nim.d.ts.map +1 -0
  179. package/types/lib/ecosystems/parsers-opam.d.ts +31 -0
  180. package/types/lib/ecosystems/parsers-opam.d.ts.map +1 -0
  181. package/types/lib/ecosystems/parsers-perl.d.ts +36 -0
  182. package/types/lib/ecosystems/parsers-perl.d.ts.map +1 -0
  183. package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
  184. package/types/lib/ecosystems/parsers-r.d.ts +35 -0
  185. package/types/lib/ecosystems/parsers-r.d.ts.map +1 -0
  186. package/types/lib/ecosystems/parsers-rebar.d.ts +42 -0
  187. package/types/lib/ecosystems/parsers-rebar.d.ts.map +1 -0
  188. package/types/lib/ecosystems/parsers-rust.d.ts +199 -0
  189. package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
  190. package/types/lib/ecosystems/parsers-spack.d.ts +33 -0
  191. package/types/lib/ecosystems/parsers-spack.d.ts.map +1 -0
  192. package/types/lib/ecosystems/parsers-stack.d.ts +35 -0
  193. package/types/lib/ecosystems/parsers-stack.d.ts.map +1 -0
  194. package/types/lib/ecosystems/parsers-terraform.d.ts +34 -0
  195. package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -0
  196. package/types/lib/ecosystems/pythonutils.d.ts +1 -1
  197. package/types/lib/ecosystems/rubyutils.d.ts +143 -2
  198. package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
  199. package/types/lib/ecosystems/rusi.d.ts +13 -0
  200. package/types/lib/ecosystems/rusi.d.ts.map +1 -1
  201. package/types/lib/evinser/evinser.d.ts +81 -2
  202. package/types/lib/evinser/evinser.d.ts.map +1 -1
  203. package/types/lib/evinser/swiftsem.d.ts +280 -15
  204. package/types/lib/evinser/swiftsem.d.ts.map +1 -1
  205. package/types/lib/helpers/agenticToolutils.d.ts +87 -0
  206. package/types/lib/helpers/agenticToolutils.d.ts.map +1 -0
  207. package/types/lib/helpers/bomSigner.d.ts +106 -13
  208. package/types/lib/helpers/bomSigner.d.ts.map +1 -1
  209. package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -1
  210. package/types/lib/inventory/aiCollector.d.ts.map +1 -1
  211. package/types/lib/inventory/aiInventory.d.ts.map +1 -1
  212. package/types/lib/inventory/analyzer.d.ts.map +1 -1
  213. package/types/lib/inventory/atomUtils.d.ts +145 -3
  214. package/types/lib/inventory/atomUtils.d.ts.map +1 -1
  215. package/types/lib/inventory/auditCategories.d.ts +4 -0
  216. package/types/lib/inventory/auditCategories.d.ts.map +1 -1
  217. package/types/lib/inventory/cbomutils.d.ts.map +1 -1
  218. package/types/lib/inventory/cdxrs.d.ts +8 -3
  219. package/types/lib/inventory/cdxrs.d.ts.map +1 -1
  220. package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
  221. package/types/lib/inventory/depsUtils.d.ts +15 -0
  222. package/types/lib/inventory/depsUtils.d.ts.map +1 -1
  223. package/types/lib/inventory/display.d.ts.map +1 -1
  224. package/types/lib/inventory/dosai.d.ts +87 -1
  225. package/types/lib/inventory/dosai.d.ts.map +1 -1
  226. package/types/lib/inventory/dynamic.d.ts.map +1 -1
  227. package/types/lib/inventory/envcontext.d.ts.map +1 -1
  228. package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
  229. package/types/lib/inventory/fetchBatch.d.ts +34 -0
  230. package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
  231. package/types/lib/inventory/fetchRate.d.ts +9 -6
  232. package/types/lib/inventory/fetchRate.d.ts.map +1 -1
  233. package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
  234. package/types/lib/inventory/jvmToolEnv.d.ts +5 -4
  235. package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
  236. package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
  237. package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -1
  238. package/types/lib/inventory/mcpServerManifest.d.ts +19 -0
  239. package/types/lib/inventory/mcpServerManifest.d.ts.map +1 -0
  240. package/types/lib/inventory/mcpbBundle.d.ts +19 -0
  241. package/types/lib/inventory/mcpbBundle.d.ts.map +1 -0
  242. package/types/lib/inventory/plugins.d.ts.map +1 -1
  243. package/types/lib/inventory/protobom.d.ts +18 -13
  244. package/types/lib/inventory/protobom.d.ts.map +1 -1
  245. package/types/lib/inventory/protobomLoader.d.ts.map +1 -1
  246. package/types/lib/inventory/purl.d.ts +71 -2
  247. package/types/lib/inventory/purl.d.ts.map +1 -1
  248. package/types/lib/inventory/sbomDocument.d.ts.map +1 -1
  249. package/types/lib/inventory/source.d.ts.map +1 -1
  250. package/types/lib/inventory/stylePackageRefs.d.ts +17 -0
  251. package/types/lib/inventory/stylePackageRefs.d.ts.map +1 -0
  252. package/types/lib/inventory/svelteUtils.d.ts +124 -0
  253. package/types/lib/inventory/svelteUtils.d.ts.map +1 -0
  254. package/types/lib/inventory/traceRunner.d.ts +47 -1
  255. package/types/lib/inventory/traceRunner.d.ts.map +1 -1
  256. package/types/lib/managers/binary.d.ts +30 -1
  257. package/types/lib/managers/binary.d.ts.map +1 -1
  258. package/types/lib/managers/docker.d.ts +29 -0
  259. package/types/lib/managers/docker.d.ts.map +1 -1
  260. package/types/lib/managers/oci.d.ts +14 -0
  261. package/types/lib/managers/oci.d.ts.map +1 -1
  262. package/types/lib/parsers/cmakeLists.d.ts +76 -0
  263. package/types/lib/parsers/cmakeLists.d.ts.map +1 -0
  264. package/types/lib/parsers/huggingfaceUtils.d.ts +8 -1
  265. package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
  266. package/types/lib/parsers/largeJson.d.ts +52 -0
  267. package/types/lib/parsers/largeJson.d.ts.map +1 -0
  268. package/types/lib/parsers/npmrc.d.ts +5 -0
  269. package/types/lib/parsers/npmrc.d.ts.map +1 -1
  270. package/types/lib/parsers/onnx.d.ts +28 -0
  271. package/types/lib/parsers/onnx.d.ts.map +1 -0
  272. package/types/lib/parsers/safetensors.d.ts +22 -0
  273. package/types/lib/parsers/safetensors.d.ts.map +1 -0
  274. package/types/lib/stages/postgen/introspection/markers.d.ts +3 -2
  275. package/types/lib/stages/postgen/introspection/markers.d.ts.map +1 -1
  276. package/types/lib/stages/postgen/postgen.d.ts +33 -0
  277. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  278. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  279. package/types/lib/validator/index.d.ts +10 -1
  280. package/types/lib/validator/index.d.ts.map +1 -1
  281. package/types/lib/validator/reporters/console.d.ts.map +1 -1
  282. package/types/lib/validator/reporters/json.d.ts.map +1 -1
  283. package/types/lib/validator/reporters/sarif.d.ts.map +1 -1
@@ -1,12 +1,29 @@
1
- import { readFileSync } from "node:fs";
2
- import { basename, dirname, join } from "node:path";
1
+ import { readFileSync, realpathSync } from "node:fs";
2
+ import {
3
+ basename,
4
+ dirname,
5
+ isAbsolute,
6
+ join,
7
+ posix,
8
+ relative,
9
+ win32,
10
+ } from "node:path";
3
11
  import process from "node:process";
4
12
 
5
13
  import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
6
- import { getAllFiles, safeExistsSync } from "../core/fs.js";
14
+ import { SWIFT_CMD } from "../core/env.js";
15
+ import {
16
+ getAllFiles,
17
+ getTmpDir,
18
+ safeExistsSync,
19
+ safeMkdtempSync,
20
+ safeRmSync,
21
+ safeSpawnSync,
22
+ } from "../core/fs.js";
7
23
  import { splitCommandArgs } from "../ecosystems/parsers-js.js";
24
+ import { collectSwiftProductionTargets } from "../ecosystems/parsers-misc.js";
8
25
  import { runSwiftCommand } from "../inventory/envcontext.js";
9
- import { executeSourcekitten } from "../managers/binary.js";
26
+ import { executeSourcekitten, probeSourcekitten } from "../managers/binary.js";
10
27
 
11
28
  // Swift entity kinds
12
29
  // https://github.com/swiftlang/swift/blob/main/tools/SourceKit/docs/SwiftSupport.txt
@@ -70,15 +87,194 @@ const IGNORABLE_TYPES = [
70
87
  "String",
71
88
  ];
72
89
 
90
+ // The Swift Build engine (default from Swift 6.4) writes per-target output
91
+ // file maps named `<target>-OutputFileMap.json` under
92
+ // `.build/out/Intermediates.noindex/`, while the llbuild engine writes
93
+ // `output-file-map.json` files next to the module artifacts.
94
+ const SWIFT_BUILD_FILEMAP_SUFFIX = "-OutputFileMap.json";
95
+ // SwiftPM turns target names into module names by replacing every character
96
+ // that is not valid in a C identifier with an underscore.
97
+ const NON_IDENTIFIER_CHARS = /[^A-Za-z0-9_]/g;
98
+ // Build logs and descriptions on Windows can mix both separators
99
+ const PATH_SEPARATORS = /[\\/]/;
100
+
101
+ /**
102
+ * Convert a SwiftPM target or package name into its module name.
103
+ *
104
+ * @param {String} name Target or package name
105
+ * @returns {String} Module name
106
+ */
107
+ export function toModuleName(name) {
108
+ return (name || "").replace(NON_IDENTIFIER_CHARS, "_");
109
+ }
110
+
111
+ /**
112
+ * Decide whether a module belongs to tests or to an aggregate product and so
113
+ * must not contribute build symbols or semantic context.
114
+ *
115
+ * @param {String} moduleName Module name
116
+ * @returns {boolean} `true` when the module should be skipped
117
+ */
118
+ export function isTestOrAggregateModule(moduleName) {
119
+ return (
120
+ !moduleName ||
121
+ moduleName.endsWith("Tests") ||
122
+ moduleName.endsWith("PackageTests") ||
123
+ moduleName.endsWith("_test_runner") ||
124
+ moduleName.endsWith("-test-runner") ||
125
+ moduleName.endsWith("_product") ||
126
+ moduleName.endsWith("-product")
127
+ );
128
+ }
129
+
130
+ /**
131
+ * Spawn the configured swift command in a directory. `SWIFT_CMD` may carry a
132
+ * prefix such as `xcrun swift`.
133
+ *
134
+ * @param {String} basePath Working directory
135
+ * @param {Array<String>} args Arguments
136
+ * @param {Object} [spawnOptions] Extra spawn options, e.g. `input`
137
+ * @returns {Object} spawn result
138
+ */
139
+ function spawnSwift(basePath, args, spawnOptions = {}) {
140
+ const [command, ...prefixArgs] = SWIFT_CMD.trim().split(" ");
141
+ return safeSpawnSync(command, [...prefixArgs, ...args], {
142
+ cwd: basePath,
143
+ ...spawnOptions,
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Parse the JSON printed by `swift -print-target-info`.
149
+ *
150
+ * `paths.runtimeResourcePath` is `<toolchain>/usr/lib/swift` for every
151
+ * toolchain layout (Xcode, `.xctoolchain` bundles, swiftly, Linux tarballs),
152
+ * so it names the toolchain that actually compiles the project.
153
+ *
154
+ * @param {Object} targetInfo Parsed `-print-target-info` output
155
+ * @returns {undefined|Object} `{ compilerVersion, triple, runtimeResourcePath, toolchainDir }`
156
+ */
157
+ export function parseSwiftTargetInfo(targetInfo) {
158
+ const runtimeResourcePath = targetInfo?.paths?.runtimeResourcePath;
159
+ if (!runtimeResourcePath) {
160
+ return undefined;
161
+ }
162
+ const libDir = dirname(runtimeResourcePath);
163
+ const usrDir = dirname(libDir);
164
+ const toolchainDir =
165
+ basename(runtimeResourcePath) === "swift" &&
166
+ basename(libDir) === "lib" &&
167
+ basename(usrDir) === "usr"
168
+ ? dirname(usrDir)
169
+ : undefined;
170
+ return {
171
+ compilerVersion: targetInfo.compilerVersion,
172
+ triple: targetInfo.target?.triple,
173
+ runtimeResourcePath,
174
+ toolchainDir,
175
+ };
176
+ }
177
+
178
+ const swiftToolchainCache = new Map();
179
+
180
+ /**
181
+ * Identify the toolchain that `swift` resolves to inside a project directory.
182
+ *
183
+ * The query runs in the project directory because toolchain managers such as
184
+ * swiftly select the toolchain per directory (`.swift-version`), so the
185
+ * compiler that builds a project can differ from the one on the PATH of the
186
+ * calling process.
187
+ *
188
+ * @param {String} basePath Project directory
189
+ * @returns {undefined|Object} Toolchain details, see parseSwiftTargetInfo
190
+ */
191
+ export function resolveSwiftToolchain(basePath) {
192
+ const cacheKey = basePath || process.cwd();
193
+ if (swiftToolchainCache.has(cacheKey)) {
194
+ return swiftToolchainCache.get(cacheKey);
195
+ }
196
+ let toolchain;
197
+ const result = spawnSwift(basePath, ["-print-target-info"]);
198
+ if (result?.status === 0 && result.stdout) {
199
+ try {
200
+ toolchain = parseSwiftTargetInfo(JSON.parse(result.stdout));
201
+ } catch (_e) {
202
+ toolchain = undefined;
203
+ }
204
+ }
205
+ swiftToolchainCache.set(cacheKey, toolchain);
206
+ return toolchain;
207
+ }
208
+
209
+ /**
210
+ * Environment overrides that make sourcekitten load the SourceKit of the
211
+ * toolchain that builds the project.
212
+ *
213
+ * Without guidance sourcekitten uses the toolchain of the selected Xcode (or
214
+ * the `swift` found on the PATH on Linux). SourceKit cannot load modules
215
+ * produced by a different compiler version, so every reference to a
216
+ * dependency then goes unresolved. On macOS sourcekitten honours
217
+ * `XCODE_DEFAULT_TOOLCHAIN_OVERRIDE` ahead of `xcrun`; on Linux it honours
218
+ * `LINUX_SOURCEKIT_LIB_PATH`. Explicitly configured values always win.
219
+ *
220
+ * @param {Object} toolchain Toolchain details from resolveSwiftToolchain
221
+ * @param {String} [platform] Platform name, defaults to process.platform
222
+ * @returns {undefined|Object} Environment overrides for sourcekitten
223
+ */
224
+ export function sourcekittenEnvForToolchain(
225
+ toolchain,
226
+ platform = process.platform,
227
+ ) {
228
+ const toolchainDir = toolchain?.toolchainDir;
229
+ if (!toolchainDir) {
230
+ return undefined;
231
+ }
232
+ if (platform === "darwin") {
233
+ if (
234
+ readEnvironmentVariable("XCODE_DEFAULT_TOOLCHAIN_OVERRIDE") ||
235
+ readEnvironmentVariable("TOOLCHAIN_DIR")
236
+ ) {
237
+ return undefined;
238
+ }
239
+ // sourcekitten loads the in-process SourceKit of `.xctoolchain` bundles
240
+ // and of the Command Line Tools alike. Open-source toolchains before
241
+ // Swift 6.1 do not ship it; sourcekitten then falls back to the Xcode
242
+ // toolchain, which cannot load the modules of this build
243
+ if (
244
+ !safeExistsSync(
245
+ join(toolchainDir, "usr", "lib", "sourcekitdInProc.framework"),
246
+ )
247
+ ) {
248
+ console.log(
249
+ `The Swift toolchain at ${toolchainDir} has no sourcekitdInProc.framework, so sourcekitten cannot use it. Swift evidence will be limited to import declarations; build with Swift 6.1 or later, or with Xcode.`,
250
+ );
251
+ return undefined;
252
+ }
253
+ return { XCODE_DEFAULT_TOOLCHAIN_OVERRIDE: toolchainDir };
254
+ }
255
+ if (platform === "linux") {
256
+ const libDir = join(toolchainDir, "usr", "lib");
257
+ if (
258
+ readEnvironmentVariable("LINUX_SOURCEKIT_LIB_PATH") ||
259
+ !safeExistsSync(join(libDir, "libsourcekitdInProc.so"))
260
+ ) {
261
+ return undefined;
262
+ }
263
+ return { LINUX_SOURCEKIT_LIB_PATH: libDir };
264
+ }
265
+ return undefined;
266
+ }
267
+
73
268
  /**
74
269
  * Retrieve the structure information of a .swift file in json format
75
270
  *
76
271
  * @param {String} filePath Path to .swift file
272
+ * @param {Object} [env] Environment overrides for sourcekitten
77
273
  *
78
274
  * @returns {undefined|Object} JSON representation of the swift file or undefined.
79
275
  */
80
- export function getStructure(filePath) {
81
- return executeSourcekitten(["structure", "--file", filePath]);
276
+ export function getStructure(filePath, env) {
277
+ return executeSourcekitten(["structure", "--file", filePath], env);
82
278
  }
83
279
 
84
280
  /**
@@ -139,214 +335,599 @@ function collectStructureTypes(substructures, refTypes) {
139
335
  }
140
336
  }
141
337
 
338
+ /**
339
+ * Split a Swift command line, as printed by a verbose build or given in an
340
+ * environment variable, into arguments.
341
+ *
342
+ * Paths on Windows use backslash separators, so a backslash there is literal
343
+ * unless it precedes a double quote (the `CommandLineToArgvW` rules). Single
344
+ * quotes, which SwiftPM uses to quote arguments on every platform, keep their
345
+ * content literal. Elsewhere shell quoting applies.
346
+ *
347
+ * @param {String} commandString Command line
348
+ * @param {String} [platform] Platform name, defaults to process.platform
349
+ * @returns {Array<String>} Arguments
350
+ */
351
+ export function splitSwiftCommandArgs(
352
+ commandString,
353
+ platform = process.platform,
354
+ ) {
355
+ const text = commandString || "";
356
+ if (platform !== "win32") {
357
+ return splitCommandArgs(text);
358
+ }
359
+ const args = [];
360
+ let current = "";
361
+ let hasArg = false;
362
+ let quote = "";
363
+ for (let i = 0; i < text.length; i++) {
364
+ const char = text[i];
365
+ if (quote === "'") {
366
+ if (char === "'") {
367
+ quote = "";
368
+ } else {
369
+ current += char;
370
+ }
371
+ continue;
372
+ }
373
+ if (char === "\\") {
374
+ let count = 1;
375
+ while (text[i + count] === "\\") {
376
+ count++;
377
+ }
378
+ hasArg = true;
379
+ if (text[i + count] !== '"') {
380
+ current += "\\".repeat(count);
381
+ i += count - 1;
382
+ continue;
383
+ }
384
+ current += "\\".repeat(Math.floor(count / 2));
385
+ if (count % 2) {
386
+ current += '"';
387
+ i += count;
388
+ } else {
389
+ i += count - 1;
390
+ }
391
+ continue;
392
+ }
393
+ if (char === '"' || (char === "'" && !quote)) {
394
+ quote = quote ? "" : char;
395
+ hasArg = true;
396
+ continue;
397
+ }
398
+ if (!quote && (char === " " || char === "\t")) {
399
+ if (hasArg) {
400
+ args.push(current);
401
+ current = "";
402
+ hasArg = false;
403
+ }
404
+ continue;
405
+ }
406
+ current += char;
407
+ hasArg = true;
408
+ }
409
+ if (hasArg) {
410
+ args.push(current);
411
+ }
412
+ return args;
413
+ }
414
+
142
415
  /**
143
416
  * Method to perform swift build in verbose mode.
144
417
  *
418
+ * `SWIFT_BUILD_ARGS` adds arguments to the build, for example
419
+ * `--build-system native` to use the llbuild engine with Swift 6.4.
420
+ *
145
421
  * @param {String} basePath Path
146
422
  * @returns {undefined|String} Verbose build output
147
423
  */
148
424
  export function verboseBuild(basePath) {
149
425
  runSwiftCommand(basePath, ["package", "clean"]);
150
426
  console.log("Extracting compiler arguments from swift build...");
151
- return runSwiftCommand(basePath, [
152
- "build",
153
- "-c",
154
- "debug",
155
- "--verbose",
156
- "-Xcc",
157
- "-Wno-error",
158
- ]);
427
+ const buildArgs = ["build", "-c", "debug", "--verbose", "-Xcc", "-Wno-error"];
428
+ const extraBuildArgs = readEnvironmentVariable("SWIFT_BUILD_ARGS");
429
+ if (extraBuildArgs) {
430
+ buildArgs.push(...splitSwiftCommandArgs(extraBuildArgs));
431
+ }
432
+ return runSwiftCommand(basePath, buildArgs);
159
433
  }
160
434
 
435
+ // Compiler arguments followed by a single value that sourcekitten needs to
436
+ // type-check a module's sources exactly as the compiler did
437
+ const SWIFT_ARGS_WITH_VALUE = new Set([
438
+ "-sdk",
439
+ "-target",
440
+ "-target-variant",
441
+ "-swift-version",
442
+ "-language-mode",
443
+ "-module-name",
444
+ "-package-name",
445
+ "-I",
446
+ "-F",
447
+ "-Isystem",
448
+ "-Fsystem",
449
+ "-vfsoverlay",
450
+ "-plugin-path",
451
+ "-external-plugin-path",
452
+ "-load-plugin-executable",
453
+ "-load-plugin-library",
454
+ "-in-process-plugin-server-path",
455
+ "-enable-upcoming-feature",
456
+ "-enable-experimental-feature",
457
+ "-disable-upcoming-feature",
458
+ "-disable-experimental-feature",
459
+ "-Xcc",
460
+ ]);
461
+ // Compiler arguments that apply to the whole module, typically single-value
462
+ // flags whose value appears once per line (`-module-cache-path` is replaced by
463
+ // a private cache)
464
+ const SWIFT_SINGLE_VALUE_ARGS = new Set([
465
+ "-sdk",
466
+ "-target",
467
+ "-target-variant",
468
+ "-swift-version",
469
+ "-language-mode",
470
+ "-module-name",
471
+ "-package-name",
472
+ ]);
473
+ const SWIFT_BOOL_ARGS = new Set([
474
+ "-parse-as-library",
475
+ "-enable-testing",
476
+ "-application-extension",
477
+ "-enable-library-evolution",
478
+ "-enable-bare-slash-regex",
479
+ "-warn-concurrency",
480
+ "-suppress-warnings",
481
+ ]);
482
+ // Frontend flags that change how sources are type-checked (macros, language
483
+ // features) and so are kept when passed through `-Xfrontend`
484
+ const SWIFT_FRONTEND_ARGS_WITH_VALUE = new Set([
485
+ "-load-plugin-executable",
486
+ "-load-plugin-library",
487
+ "-plugin-path",
488
+ "-external-plugin-path",
489
+ "-enable-upcoming-feature",
490
+ "-enable-experimental-feature",
491
+ ]);
492
+ // Pass-through flags whose value belongs to another tool
493
+ const SWIFT_PASSTHROUGH_ARGS = new Set([
494
+ "-Xfrontend",
495
+ "-Xllvm",
496
+ "-Xlinker",
497
+ "-Xclang-linker",
498
+ ]);
499
+
161
500
  /**
162
- * Method to parse the verbose swift build output to identify key compiler parameters.
501
+ * Reduce one module's compiler invocation to the arguments that affect how
502
+ * its sources are type-checked.
163
503
  *
164
- * @param {String} buildOutput Verbose build output
165
- * @returns {Object} compiler build parameters
504
+ * Only known argument classes are kept, so outputs, incremental build
505
+ * bookkeeping, response files, and flags a SourceKit build may not know are
506
+ * dropped. The order of the kept arguments is preserved, which keeps paired
507
+ * clang arguments such as `-Xcc -I -Xcc <path>` intact. The module cache is
508
+ * dropped as well: the build's cache may hold modules of other toolchains.
509
+ *
510
+ * @param {Array<String>} tokens Tokenized compiler invocation
511
+ * @returns {Array<String>} Sanitized compiler arguments
166
512
  */
167
- export function extractCompilerParamsFromBuild(buildOutput) {
168
- const params = {};
169
- if (!buildOutput) {
170
- console.log("No build output. Returning empty compilerArgs");
171
- return { params, compilerArgs: [] };
172
- }
173
- const singletonArgs = new Set([
174
- "-sdk",
175
- "-target",
176
- "-swift-version",
177
- "-package-description-version",
178
- "-module-cache-path",
179
- ]);
180
- const cumulativeArgs = new Set([
181
- "-F",
182
- "-I",
183
- "-L",
184
- "-vfsoverlay",
185
- "-Xllvm",
186
- "-external-plugin-path",
187
- "-plugin-path",
188
- ]);
189
- const orderedArgs = new Set(["-Xcc"]);
190
- const boolArgs = new Set([
191
- "-parse-as-library",
192
- "-incremental",
193
- "-track-system-dependencies",
194
- "-suppress-remarks",
195
- "-suppress-warnings",
196
- "-stack-check",
197
- "-disable-clang-spi",
198
- "-no-color-diagnostics",
199
- "-enable-testing",
200
- "-enable-library-evolution",
201
- ]);
202
- const ignoredArgs = new Set([
203
- "-o",
204
- "-output-file-map",
205
- "-emit-module-path",
206
- "-emit-module-doc-path",
207
- "-emit-dependencies-path",
208
- "-emit-reference-dependencies-path",
209
- "-emit-objc-header-path",
210
- "-primary-file",
211
- "-main-file",
212
- "-num-threads",
213
- "-enable-objc-interop",
214
- "-empty-abi-descriptor",
215
- "-target-sdk-version",
216
- "-target-sdk-name",
217
- "-incremental",
218
- "-module-name",
219
- "-j",
220
- ]);
221
-
222
- /**
223
- * Validates a parameter value to ensure it's not a garbage string or full command.
224
- */
225
- const isValidParamValue = (val) => {
226
- if (!val) return false;
227
- if (val.length > 2048) return false;
228
- return !(val.includes(" -cc1") || val.includes('clang"'));
513
+ export function sanitizeSwiftModuleArgs(tokens) {
514
+ const args = [];
515
+ const seenPairs = new Set();
516
+ const seenSingles = new Set();
517
+ const pushPair = (flag, value) => {
518
+ // Repeated include paths are harmless but noisy; `-Xcc` values are
519
+ // positional (`-Xcc -I -Xcc <path>`) and must never be de-duplicated
520
+ if (flag !== "-Xcc") {
521
+ const key = `${flag}\u0000${value}`;
522
+ if (seenPairs.has(key)) {
523
+ return;
524
+ }
525
+ seenPairs.add(key);
526
+ }
527
+ args.push(flag, value);
229
528
  };
230
-
231
- const lines = buildOutput.split("\n");
232
- let bestLine = "";
233
- for (let l of lines) {
234
- l = l.trim();
235
- if (!l.includes("swiftc") && !l.includes("swift-frontend")) {
529
+ for (let i = 0; i < tokens.length; i++) {
530
+ const token = tokens[i];
531
+ if (SWIFT_PASSTHROUGH_ARGS.has(token)) {
532
+ const value = tokens[i + 1];
533
+ i++;
534
+ if (token !== "-Xfrontend" || !value) {
535
+ continue;
536
+ }
537
+ if (SWIFT_FRONTEND_ARGS_WITH_VALUE.has(value)) {
538
+ if (tokens[i + 1] === "-Xfrontend" && tokens[i + 2] !== undefined) {
539
+ args.push("-Xfrontend", value, "-Xfrontend", tokens[i + 2]);
540
+ i += 2;
541
+ }
542
+ } else if (value.startsWith("-strict-concurrency=")) {
543
+ args.push("-Xfrontend", value);
544
+ }
545
+ continue;
546
+ }
547
+ if (SWIFT_ARGS_WITH_VALUE.has(token)) {
548
+ const value = tokens[i + 1];
549
+ i++;
550
+ if (value === undefined) {
551
+ continue;
552
+ }
553
+ if (SWIFT_SINGLE_VALUE_ARGS.has(token)) {
554
+ if (seenSingles.has(token)) {
555
+ continue;
556
+ }
557
+ seenSingles.add(token);
558
+ }
559
+ pushPair(token, value);
560
+ continue;
561
+ }
562
+ if (SWIFT_BOOL_ARGS.has(token)) {
563
+ if (!seenSingles.has(token)) {
564
+ seenSingles.add(token);
565
+ args.push(token);
566
+ }
236
567
  continue;
237
568
  }
238
- if (l === "/usr/bin/swift-frontend" || l.endsWith("swift-frontend")) {
569
+ if (token.startsWith("-strict-concurrency=")) {
570
+ args.push(token);
239
571
  continue;
240
572
  }
241
- if (l.length > bestLine.length) {
242
- bestLine = l;
573
+ if (token === "-D" && tokens[i + 1] !== undefined) {
574
+ pushPair("-D", tokens[i + 1]);
575
+ i++;
576
+ continue;
577
+ }
578
+ // Joined forms: -DFOO, -I/path, -F/path
579
+ if (token.length > 2 && ["-D", "-I", "-F"].includes(token.slice(0, 2))) {
580
+ if (!token.startsWith("-Isystem") && !token.startsWith("-Fsystem")) {
581
+ pushPair(token.slice(0, 2), token.slice(2));
582
+ }
243
583
  }
244
584
  }
585
+ return args;
586
+ }
587
+
588
+ /**
589
+ * Read the sources listed in a response or file-list file (`@<path>`), one
590
+ * path per line.
591
+ *
592
+ * @param {String} listFile File list path
593
+ * @param {String} [platform] Platform that wrote the list
594
+ * @returns {Array<String>} Swift source paths
595
+ */
596
+ function readSourceList(listFile, platform = process.platform) {
597
+ if (!listFile || !safeExistsSync(listFile)) {
598
+ return [];
599
+ }
600
+ try {
601
+ return readFileSync(listFile, { encoding: "utf-8" })
602
+ .split("\n")
603
+ .map((l) => splitSwiftCommandArgs(l.trim(), platform)[0])
604
+ .filter((l) => l?.endsWith(".swift"));
605
+ } catch (_e) {
606
+ return [];
607
+ }
608
+ }
245
609
 
246
- // Parse only the best line found
247
- if (bestLine) {
248
- const tokens = splitCommandArgs(bestLine);
249
- for (let i = 0; i < tokens.length; i++) {
250
- const token = tokens[i];
251
- if (ignoredArgs.has(token)) {
252
- if (i + 1 < tokens.length && !tokens[i + 1].startsWith("-")) {
253
- i++;
610
+ /**
611
+ * Extract the per-module compiler invocations printed by a verbose swift
612
+ * build.
613
+ *
614
+ * Both engines print one driver invocation per module that carries
615
+ * `-module-name`: llbuild prints `swiftc ... -module-name X ... @sources`,
616
+ * while the Swift Build engine prints `builtin-SwiftDriver -- swiftc ...
617
+ * -module-name X ... @<target>.SwiftFileList`. llbuild also prints the
618
+ * frontend jobs of each module, which are only used when no driver line is
619
+ * found. Package manifest and plugin compilations are skipped.
620
+ *
621
+ * @param {String} buildOutput Verbose build output
622
+ * @param {String} [platform] Platform that printed the output, which decides
623
+ * its quoting rules; defaults to process.platform
624
+ * @returns {Object} Map of module name to `{ args, sources, isDriver }`
625
+ */
626
+ export function parseSwiftModuleInvocations(
627
+ buildOutput,
628
+ platform = process.platform,
629
+ ) {
630
+ const isAbsolutePath =
631
+ platform === "win32" ? win32.isAbsolute : posix.isAbsolute;
632
+ const modules = {};
633
+ for (const rawLine of (buildOutput || "").split("\n")) {
634
+ const line = rawLine.trim();
635
+ if (!line.includes("swiftc") && !line.includes("swift-frontend")) {
636
+ continue;
637
+ }
638
+ if (!line.includes("-module-name")) {
639
+ continue;
640
+ }
641
+ const tokens = splitSwiftCommandArgs(line, platform);
642
+ const nameIndex = tokens.indexOf("-module-name");
643
+ const moduleName = tokens[nameIndex + 1];
644
+ if (
645
+ !moduleName ||
646
+ tokens.includes("-package-description-version") ||
647
+ tokens.includes("-plugin-description-version")
648
+ ) {
649
+ continue;
650
+ }
651
+ const isDriver = !tokens.includes("-frontend");
652
+ const sources = new Set();
653
+ for (const token of tokens) {
654
+ if (token.startsWith("@")) {
655
+ for (const src of readSourceList(token.slice(1), platform)) {
656
+ sources.add(src);
254
657
  }
658
+ } else if (token.endsWith(".swift") && isAbsolutePath(token)) {
659
+ sources.add(token);
660
+ }
661
+ }
662
+ const existing = modules[moduleName];
663
+ // Prefer the driver invocation, then the longest frontend job
664
+ if (
665
+ existing &&
666
+ (existing.isDriver > isDriver ||
667
+ (existing.isDriver === isDriver && existing.length >= tokens.length))
668
+ ) {
669
+ for (const src of sources) {
670
+ if (!existing.sources.includes(src)) {
671
+ existing.sources.push(src);
672
+ }
673
+ }
674
+ continue;
675
+ }
676
+ const mergedSources = new Set([...(existing?.sources || []), ...sources]);
677
+ modules[moduleName] = {
678
+ args: sanitizeSwiftModuleArgs(tokens),
679
+ sources: Array.from(mergedSources),
680
+ isDriver,
681
+ length: tokens.length,
682
+ };
683
+ }
684
+ for (const amodule of Object.values(modules)) {
685
+ delete amodule.length;
686
+ }
687
+ return modules;
688
+ }
689
+
690
+ /**
691
+ * Parse the llbuild engine's build description
692
+ * (`.build/<triple>/debug/description.json`).
693
+ *
694
+ * Its `swiftCommands` record every Swift module the build compiled, with the
695
+ * exact source list and compiler arguments. Host-tool variants
696
+ * (`...-tool.module`, built for macros and plugins) are used only when a
697
+ * module has no regular variant.
698
+ *
699
+ * @param {Object} descriptionJson Parsed description.json
700
+ * @returns {Object} Map of module name to `{ args, sources, isDriver }`
701
+ */
702
+ export function parseSwiftBuildDescription(descriptionJson) {
703
+ const modules = {};
704
+ const toolVariants = new Set();
705
+ for (const [key, command] of Object.entries(
706
+ descriptionJson?.swiftCommands || {},
707
+ )) {
708
+ const moduleName = command?.moduleName;
709
+ if (!moduleName) {
710
+ continue;
711
+ }
712
+ const isTool = key.endsWith("-tool.module");
713
+ if (modules[moduleName] && (isTool || !toolVariants.has(moduleName))) {
714
+ continue;
715
+ }
716
+ const tokens = [
717
+ "-module-name",
718
+ moduleName,
719
+ ...(command.otherArguments || []),
720
+ ];
721
+ if (command.importPath) {
722
+ tokens.push("-I", command.importPath);
723
+ }
724
+ modules[moduleName] = {
725
+ args: sanitizeSwiftModuleArgs(tokens),
726
+ sources: (command.sources || []).filter((s) => s.endsWith(".swift")),
727
+ isDriver: true,
728
+ };
729
+ if (isTool) {
730
+ toolVariants.add(moduleName);
731
+ } else {
732
+ toolVariants.delete(moduleName);
733
+ }
734
+ }
735
+ return modules;
736
+ }
737
+
738
+ // Clang argument groups: a flag and the separate value that follows it
739
+ const CLANG_ARGS_WITH_SEPARATE_VALUE = new Set([
740
+ "-I",
741
+ "-F",
742
+ "-D",
743
+ "-U",
744
+ "-isystem",
745
+ "-iquote",
746
+ "-idirafter",
747
+ "-include",
748
+ "-iframework",
749
+ "-ivfsoverlay",
750
+ "-working-directory",
751
+ "-Xclang",
752
+ "-target",
753
+ ]);
754
+
755
+ /**
756
+ * Split a list of `-Xcc` values into clang argument groups so that repeated
757
+ * groups can be dropped without separating a flag from its value.
758
+ *
759
+ * @param {Array<String>} values `-Xcc` values in order
760
+ * @returns {Array<Array<String>>} Clang argument groups
761
+ */
762
+ function groupClangArgs(values) {
763
+ const groups = [];
764
+ for (let i = 0; i < values.length; i++) {
765
+ if (
766
+ CLANG_ARGS_WITH_SEPARATE_VALUE.has(values[i]) &&
767
+ i + 1 < values.length
768
+ ) {
769
+ groups.push([values[i], values[i + 1]]);
770
+ i++;
771
+ } else {
772
+ groups.push([values[i]]);
773
+ }
774
+ }
775
+ return groups;
776
+ }
777
+
778
+ /**
779
+ * Merge the sanitized arguments of several modules into one argument list,
780
+ * used when a source file cannot be attributed to a single module. The first
781
+ * module's single-value flags win, include paths accumulate, and `-Xcc`
782
+ * values are merged per clang argument group.
783
+ *
784
+ * @param {Array<Array<String>>} argLists Sanitized argument lists, most relevant first
785
+ * @returns {Array<String>} Merged arguments
786
+ */
787
+ export function mergeSwiftModuleArgs(argLists) {
788
+ const merged = [];
789
+ const seen = new Set();
790
+ const seenSingles = new Set();
791
+ const clangGroups = [];
792
+ const seenClangGroups = new Set();
793
+ for (const args of argLists) {
794
+ const xccValues = [];
795
+ for (let i = 0; i < args.length; i++) {
796
+ const token = args[i];
797
+ if (token === "-Xcc") {
798
+ xccValues.push(args[i + 1]);
799
+ i++;
255
800
  continue;
256
801
  }
257
- if (singletonArgs.has(token)) {
258
- if (i + 1 < tokens.length) {
259
- const val = tokens[i + 1];
260
- if (isValidParamValue(val)) {
261
- params[token] = val;
262
- }
263
- i++;
802
+ if (SWIFT_ARGS_WITH_VALUE.has(token) || token === "-D") {
803
+ const value = args[i + 1];
804
+ i++;
805
+ if (token === "-module-name") {
806
+ continue;
264
807
  }
265
- } else if (orderedArgs.has(token)) {
266
- if (i + 1 < tokens.length) {
267
- const val = tokens[i + 1];
268
- if (isValidParamValue(val)) {
269
- if (!params[token]) {
270
- params[token] = [];
271
- }
272
- params[token].push(val);
808
+ if (SWIFT_SINGLE_VALUE_ARGS.has(token)) {
809
+ if (seenSingles.has(token)) {
810
+ continue;
273
811
  }
274
- i++;
812
+ seenSingles.add(token);
275
813
  }
276
- } else if (cumulativeArgs.has(token)) {
277
- if (i + 1 < tokens.length) {
278
- const val = tokens[i + 1];
279
- if (isValidParamValue(val)) {
280
- if (!params[token]) {
281
- params[token] = new Set();
282
- }
283
- params[token].add(val);
284
- }
285
- i++;
814
+ const key = `${token}\u0000${value}`;
815
+ if (!seen.has(key)) {
816
+ seen.add(key);
817
+ merged.push(token, value);
286
818
  }
287
- } else if (boolArgs.has(token)) {
288
- params[token] = true;
289
- } else if (token.startsWith("-D")) {
290
- if (!params["-D"]) params["-D"] = new Set();
291
- if (token === "-D" && i + 1 < tokens.length) {
292
- params["-D"].add(tokens[i + 1]);
293
- i++;
294
- } else {
295
- params["-D"].add(token.substring(2));
819
+ continue;
820
+ }
821
+ if (token === "-Xfrontend") {
822
+ // -Xfrontend groups were validated while sanitizing
823
+ const group = [token, args[i + 1]];
824
+ i++;
825
+ if (
826
+ SWIFT_FRONTEND_ARGS_WITH_VALUE.has(group[1]) &&
827
+ args[i + 1] === "-Xfrontend"
828
+ ) {
829
+ group.push(args[i + 1], args[i + 2]);
830
+ i += 2;
831
+ }
832
+ const key = group.join("\u0000");
833
+ if (!seen.has(key)) {
834
+ seen.add(key);
835
+ merged.push(...group);
296
836
  }
837
+ continue;
838
+ }
839
+ if (!seen.has(token)) {
840
+ seen.add(token);
841
+ merged.push(token);
842
+ }
843
+ }
844
+ for (const group of groupClangArgs(xccValues)) {
845
+ const key = group.join("\u0000");
846
+ if (!seenClangGroups.has(key)) {
847
+ seenClangGroups.add(key);
848
+ clangGroups.push(group);
297
849
  }
298
850
  }
299
851
  }
300
- const compilerArgsList = [];
852
+ for (const group of clangGroups) {
853
+ for (const value of group) {
854
+ merged.push("-Xcc", value);
855
+ }
856
+ }
857
+ return merged;
858
+ }
859
+
860
+ /**
861
+ * Method to parse the verbose swift build output to identify key compiler
862
+ * parameters. The arguments of the root package's modules are merged, or of
863
+ * every module when the root modules are unknown.
864
+ *
865
+ * @param {String} buildOutput Verbose build output
866
+ * @param {Array<String>|Set<String>} [rootModules] Module names of the root package
867
+ * @returns {Object} `{ params, compilerArgs }` where params maps module names to their arguments
868
+ */
869
+ export function extractCompilerParamsFromBuild(buildOutput, rootModules) {
870
+ if (!buildOutput) {
871
+ console.log("No build output. Returning empty compilerArgs");
872
+ return { params: {}, compilerArgs: [] };
873
+ }
874
+ const invocations = parseSwiftModuleInvocations(buildOutput);
875
+ const rootSet = new Set(rootModules || []);
876
+ let selected = Object.keys(invocations).filter((m) => rootSet.has(m));
877
+ if (!selected.length) {
878
+ selected = Object.keys(invocations);
879
+ }
880
+ selected.sort(
881
+ (a, b) => invocations[b].args.length - invocations[a].args.length,
882
+ );
883
+ const compilerArgs = [];
301
884
  if (process?.env?.SWIFT_COMPILER_EXTRA_ARGS) {
302
- compilerArgsList.push(...process.env.SWIFT_COMPILER_EXTRA_ARGS.split(" "));
885
+ compilerArgs.push(
886
+ ...splitSwiftCommandArgs(process.env.SWIFT_COMPILER_EXTRA_ARGS),
887
+ );
303
888
  }
304
- for (const key of Object.keys(params)) {
305
- const value = params[key];
306
- if (value instanceof Set) {
307
- for (const v of Array.from(value)) {
308
- compilerArgsList.push(key);
309
- compilerArgsList.push(v);
310
- }
311
- } else if (Array.isArray(value)) {
312
- for (const v of value) {
313
- compilerArgsList.push(key);
314
- compilerArgsList.push(v);
315
- }
316
- } else if (typeof value === "string") {
317
- compilerArgsList.push(key);
318
- compilerArgsList.push(value);
319
- } else if (value === true) {
320
- compilerArgsList.push(key);
321
- }
889
+ compilerArgs.push(
890
+ ...mergeSwiftModuleArgs(selected.map((m) => invocations[m].args)),
891
+ );
892
+ const params = {};
893
+ for (const m of selected) {
894
+ params[m] = invocations[m].args;
322
895
  }
323
- return { params, compilerArgs: compilerArgsList };
896
+ return { params, compilerArgs };
324
897
  }
325
898
 
326
899
  /**
327
900
  * Method to index a swift file and extract metadata
328
901
  *
329
902
  * @param {String} filePath Path to .swift file
330
- * @param {String|Array<string>} compilerArgs Compiler arguments extracted from verbose build log
903
+ * @param {String|Array<string>} compilerArgs Compiler arguments. They should
904
+ * list every source of the file's module so that references across files
905
+ * resolve; the file itself is appended when missing.
906
+ * @param {Object} [env] Environment overrides for sourcekitten
331
907
  * @returns {undefined|Object} metadata
332
908
  */
333
- export function index(filePath, compilerArgs) {
909
+ export function index(filePath, compilerArgs, env) {
334
910
  const skArgs = ["index", "--file", filePath];
335
- if (compilerArgs) {
336
- skArgs.push("--");
337
- if (Array.isArray(compilerArgs)) {
338
- skArgs.push(...compilerArgs);
339
- } else {
340
- skArgs.push(...compilerArgs.split(" "));
341
- }
911
+ const args = Array.isArray(compilerArgs)
912
+ ? compilerArgs
913
+ : compilerArgs
914
+ ? splitSwiftCommandArgs(compilerArgs)
915
+ : [];
916
+ skArgs.push("--", ...args);
917
+ if (!args.includes(filePath)) {
918
+ skArgs.push(filePath);
342
919
  }
343
- skArgs.push(filePath);
344
- return executeSourcekitten(skArgs);
920
+ return executeSourcekitten(skArgs, env);
345
921
  }
346
922
 
347
923
  /**
348
924
  * Parse the data from the index command
349
925
  *
926
+ * Besides the symbol names and lines, the unified symbol resolution (USR) of
927
+ * every indexed entity is recorded with its lines. A USR identifies the
928
+ * declaration the compiler resolved a reference to, including the module
929
+ * that declares it, which is what attributes a usage to a dependency.
930
+ *
350
931
  * @param {Object} indexJson Json from the index command
351
932
  * @returns {Object|undefined} Parsed value
352
933
  */
@@ -366,16 +947,20 @@ export function parseIndex(indexJson) {
366
947
  const obfuscatedSymbols = {};
367
948
  // Line numbers where the given symbols are found
368
949
  const symbolLocations = {};
950
+ // Line numbers of every resolved entity, keyed by its USR
951
+ const usrLines = {};
369
952
  buildIndexedObfuscatedSymbols(
370
953
  indexJson["key.entities"],
371
954
  obfuscatedSymbols,
372
955
  symbolLocations,
956
+ usrLines,
373
957
  );
374
958
  return {
375
959
  swiftModules: Array.from(swiftModules).sort(),
376
960
  clangModules: Array.from(clangModules).sort(),
377
961
  obfuscatedSymbols,
378
962
  symbolLocations,
963
+ usrLines,
379
964
  };
380
965
  }
381
966
 
@@ -387,7 +972,7 @@ export function parseIndex(indexJson) {
387
972
  * @param clangModules {Set<String>} clang modules
388
973
  */
389
974
  function collectIndexedModules(dependencies, swiftModules, clangModules) {
390
- for (const adep of dependencies) {
975
+ for (const adep of dependencies || []) {
391
976
  if (adep["key.kind"] === SWIFT_ENTITY_KINDS.IMPORT_SWIFT) {
392
977
  swiftModules.add(adep["key.name"]);
393
978
  } else if (adep["key.kind"] === SWIFT_ENTITY_KINDS.IMPORT_CLANG) {
@@ -409,11 +994,13 @@ function collectIndexedModules(dependencies, swiftModules, clangModules) {
409
994
  * @param entities {Array} Entities found in the index data
410
995
  * @param obfuscatedSymbols {Object} Obfuscated symbols map
411
996
  * @param symbolLocations {Object} Symbol locations
997
+ * @param usrLines {Object} Lines of each USR
412
998
  */
413
999
  function buildIndexedObfuscatedSymbols(
414
1000
  entities,
415
1001
  obfuscatedSymbols,
416
1002
  symbolLocations,
1003
+ usrLines,
417
1004
  ) {
418
1005
  if (!entities) {
419
1006
  return;
@@ -423,18 +1010,27 @@ function buildIndexedObfuscatedSymbols(
423
1010
  obfuscatedSymbols[aentity["key.name"]] = aentity["key.usr"];
424
1011
  }
425
1012
  if (aentity["key.line"]) {
426
- const symbolLocationsKey = aentity["key.name"] || aentity["key.usr"];
427
- if (!symbolLocationsKey) {
428
- continue;
429
- }
430
- if (
431
- !symbolLocations[symbolLocationsKey] ||
432
- !Array.isArray(symbolLocations[symbolLocationsKey])
433
- ) {
434
- symbolLocations[symbolLocationsKey] = [];
1013
+ if (aentity["key.usr"]) {
1014
+ if (!usrLines[aentity["key.usr"]]) {
1015
+ usrLines[aentity["key.usr"]] = [];
1016
+ }
1017
+ if (!usrLines[aentity["key.usr"]].includes(aentity["key.line"])) {
1018
+ usrLines[aentity["key.usr"]].push(aentity["key.line"]);
1019
+ }
435
1020
  }
436
- if (!symbolLocations[symbolLocationsKey].includes(aentity["key.line"])) {
437
- symbolLocations[symbolLocationsKey].push(aentity["key.line"]);
1021
+ const symbolLocationsKey = aentity["key.name"] || aentity["key.usr"];
1022
+ if (symbolLocationsKey) {
1023
+ if (
1024
+ !symbolLocations[symbolLocationsKey] ||
1025
+ !Array.isArray(symbolLocations[symbolLocationsKey])
1026
+ ) {
1027
+ symbolLocations[symbolLocationsKey] = [];
1028
+ }
1029
+ if (
1030
+ !symbolLocations[symbolLocationsKey].includes(aentity["key.line"])
1031
+ ) {
1032
+ symbolLocations[symbolLocationsKey].push(aentity["key.line"]);
1033
+ }
438
1034
  }
439
1035
  }
440
1036
  if (aentity["key.entities"]) {
@@ -442,33 +1038,204 @@ function buildIndexedObfuscatedSymbols(
442
1038
  aentity["key.entities"],
443
1039
  obfuscatedSymbols,
444
1040
  symbolLocations,
1041
+ usrLines,
445
1042
  );
446
1043
  }
447
1044
  }
448
1045
  }
449
1046
 
1047
+ // Declaration qualifiers the demangler prints ahead of the qualified name
1048
+ const DEMANGLED_QUALIFIERS = new Set([
1049
+ "static",
1050
+ "class",
1051
+ "mutating",
1052
+ "nonmutating",
1053
+ "__owned",
1054
+ "__shared",
1055
+ "__consuming",
1056
+ "borrowing",
1057
+ "consuming",
1058
+ "dynamic",
1059
+ "distributed",
1060
+ "nonisolated",
1061
+ "isolated",
1062
+ ]);
1063
+
1064
+ function isIdentifierChar(char) {
1065
+ return (
1066
+ (char >= "a" && char <= "z") ||
1067
+ (char >= "A" && char <= "Z") ||
1068
+ (char >= "0" && char <= "9") ||
1069
+ char === "_"
1070
+ );
1071
+ }
1072
+
450
1073
  /**
451
- * Method to execute dump-package package command.
1074
+ * Identify the module that declares an entity from its demangled name.
452
1075
  *
453
- * @param {String} basePath Path
454
- * @returns {undefined|Object} Output from dump-package command
1076
+ * Qualified names start with the module (`Yams.dump(object:...)`), and
1077
+ * members declared in extensions carry the extending module
1078
+ * (`(extension in Yams):Swift.String.yaml`), which is what a usage of the
1079
+ * member depends on.
1080
+ *
1081
+ * @param {String} demangled Output of `swift demangle --compact`
1082
+ * @returns {undefined|String} Module name
455
1083
  */
456
- export function dumpPackage(basePath) {
457
- const cmdOutput = runSwiftCommand(basePath, ["package", "dump-package"]);
458
- if (!cmdOutput) {
1084
+ export function moduleFromDemangledName(demangled) {
1085
+ let text = (demangled || "").trim();
1086
+ if (!text || text.startsWith("$")) {
459
1087
  return undefined;
460
1088
  }
461
- try {
462
- return JSON.parse(cmdOutput);
463
- } catch (_e) {
464
- return undefined;
1089
+ for (let round = 0; round < 4; round++) {
1090
+ const space = text.indexOf(" ");
1091
+ if (space > 0 && DEMANGLED_QUALIFIERS.has(text.slice(0, space))) {
1092
+ text = text.slice(space + 1);
1093
+ continue;
1094
+ }
1095
+ // Descriptions such as `default argument 0 of X` or
1096
+ // `variable initialization expression of X`
1097
+ const of = text.indexOf(" of ");
1098
+ const prefix = of > 0 ? text.slice(0, of) : "";
1099
+ if (prefix && !prefix.includes(".") && !prefix.includes("(")) {
1100
+ text = text.slice(of + 4);
1101
+ continue;
1102
+ }
1103
+ break;
465
1104
  }
1105
+ const extensionPrefix = "(extension in ";
1106
+ if (text.startsWith(extensionPrefix)) {
1107
+ const end = text.indexOf("):");
1108
+ return end > extensionPrefix.length
1109
+ ? text.slice(extensionPrefix.length, end)
1110
+ : undefined;
1111
+ }
1112
+ let i = 0;
1113
+ while (i < text.length && isIdentifierChar(text[i])) {
1114
+ i++;
1115
+ }
1116
+ return i > 0 && text[i] === "." ? text.slice(0, i) : undefined;
466
1117
  }
467
1118
 
468
1119
  /**
469
- * Parse the data from dump-package command
1120
+ * Identify the module of a mangled Swift name without the demangler. Only the
1121
+ * leading module context is decoded, so extension members are attributed to
1122
+ * the extended type's module.
470
1123
  *
471
- * @param {Object} dumpJson Json from dump-package command
1124
+ * @param {String} mangled Mangled name starting with `$s`
1125
+ * @returns {undefined|String} Module name
1126
+ */
1127
+ export function moduleFromMangledPrefix(mangled) {
1128
+ if (!mangled?.startsWith("$s")) {
1129
+ return undefined;
1130
+ }
1131
+ const rest = mangled.slice(2);
1132
+ if (rest.startsWith("s") || /^S[A-Za-z]/.test(rest)) {
1133
+ return rest.startsWith("So") || rest.startsWith("SC") ? "__C" : "Swift";
1134
+ }
1135
+ let i = 0;
1136
+ while (i < rest.length && rest[i] >= "0" && rest[i] <= "9") {
1137
+ i++;
1138
+ }
1139
+ // A leading 0 marks an identifier with word substitutions
1140
+ if (!i || rest[0] === "0") {
1141
+ return undefined;
1142
+ }
1143
+ const length = Number.parseInt(rest.slice(0, i), 10);
1144
+ const identifier = rest.slice(i, i + length);
1145
+ return identifier.length === length ? identifier : undefined;
1146
+ }
1147
+
1148
+ /**
1149
+ * Convert a Swift USR (`s:<mangled>`, or `s:e:s:<mangled>` for extensions)
1150
+ * into a mangled name the demangler accepts.
1151
+ *
1152
+ * @param {String} usr USR
1153
+ * @returns {undefined|String} Mangled name starting with `$s`
1154
+ */
1155
+ function swiftUsrToMangledName(usr) {
1156
+ let value = usr || "";
1157
+ if (value.startsWith("s:e:")) {
1158
+ value = value.slice(4);
1159
+ }
1160
+ return value.startsWith("s:") && value.length > 2
1161
+ ? `$s${value.slice(2)}`
1162
+ : undefined;
1163
+ }
1164
+
1165
+ /**
1166
+ * Identify the declaring module of each USR.
1167
+ *
1168
+ * Swift USRs are demangled in one batch with `swift demangle`; clang module
1169
+ * references (`c:@M@<module>`, emitted for import declarations) carry the
1170
+ * module name directly. Other clang USRs name no module and are skipped.
1171
+ *
1172
+ * @param {Array<String>} usrs USRs to resolve
1173
+ * @param {String} basePath Directory to run the demangler in
1174
+ * @returns {Map<String, String>} USR to module name
1175
+ */
1176
+ export function resolveUsrModules(usrs, basePath) {
1177
+ const usrModules = new Map();
1178
+ const pending = [];
1179
+ for (const usr of new Set(usrs)) {
1180
+ if (usr.startsWith("c:@M@")) {
1181
+ const moduleName = usr.slice(5).split("@")[0];
1182
+ if (moduleName) {
1183
+ usrModules.set(usr, moduleName);
1184
+ }
1185
+ continue;
1186
+ }
1187
+ const mangled = swiftUsrToMangledName(usr);
1188
+ if (mangled) {
1189
+ pending.push([usr, mangled]);
1190
+ }
1191
+ }
1192
+ if (!pending.length) {
1193
+ return usrModules;
1194
+ }
1195
+ const result = spawnSwift(basePath, ["demangle", "--compact"], {
1196
+ input: `${pending.map(([, mangled]) => mangled).join("\n")}\n`,
1197
+ });
1198
+ const demangledLines =
1199
+ result?.status === 0 && result.stdout ? result.stdout.split("\n") : [];
1200
+ if (!demangledLines.length && DEBUG_MODE) {
1201
+ console.log(
1202
+ "swift demangle is unavailable. Falling back to the mangled module prefix.",
1203
+ );
1204
+ }
1205
+ pending.forEach(([usr, mangled], i) => {
1206
+ const moduleName =
1207
+ moduleFromDemangledName(demangledLines[i]) ||
1208
+ moduleFromMangledPrefix(mangled);
1209
+ if (moduleName) {
1210
+ usrModules.set(usr, moduleName);
1211
+ }
1212
+ });
1213
+ return usrModules;
1214
+ }
1215
+
1216
+ /**
1217
+ * Method to execute dump-package package command.
1218
+ *
1219
+ * @param {String} basePath Path
1220
+ * @returns {undefined|Object} Output from dump-package command
1221
+ */
1222
+ export function dumpPackage(basePath) {
1223
+ // stderr carries manifest warnings, so only stdout is parsed
1224
+ const result = spawnSwift(basePath, ["package", "dump-package"]);
1225
+ if (!result?.stdout) {
1226
+ return undefined;
1227
+ }
1228
+ try {
1229
+ return JSON.parse(result.stdout);
1230
+ } catch (_e) {
1231
+ return undefined;
1232
+ }
1233
+ }
1234
+
1235
+ /**
1236
+ * Parse the data from dump-package command
1237
+ *
1238
+ * @param {Object} dumpJson Json from dump-package command
472
1239
  * @returns {Object|undefined} Parsed value
473
1240
  */
474
1241
  export function parseDumpPackage(dumpJson) {
@@ -481,12 +1248,29 @@ export function parseDumpPackage(dumpJson) {
481
1248
  platforms: dumpJson?.platforms,
482
1249
  };
483
1250
  const rootPkgDependencies = [];
1251
+ const rootModules = new Set();
1252
+ // Test targets and the library targets only tests depend on
1253
+ const testModules = new Set();
1254
+ const productionTargets = collectSwiftProductionTargets(dumpJson);
484
1255
  if (dumpJson.targets) {
485
1256
  for (const atarget of dumpJson.targets) {
486
- const ref = atarget.name.replace("+", "_");
1257
+ const ref = atarget.name.replaceAll("+", "_");
1258
+ if (productionTargets.has(String(atarget.name).toLowerCase())) {
1259
+ rootModules.add(toModuleName(ref));
1260
+ } else {
1261
+ testModules.add(toModuleName(ref));
1262
+ }
487
1263
  if (atarget.dependencies) {
1264
+ // Product dependencies such as `.product(name:package:)` record both
1265
+ // the product (module) name and the package identity; `byName` and
1266
+ // `target` entries only record the product or target name.
488
1267
  const dependsOn = atarget.dependencies
489
- .map((v) => v?.byName?.[0].replace("+", "_"))
1268
+ .map(
1269
+ (v) =>
1270
+ v?.byName?.[0]?.replaceAll("+", "_") ??
1271
+ v?.target?.[0]?.replaceAll("+", "_") ??
1272
+ v?.product?.[0]?.replaceAll("+", "_"),
1273
+ )
490
1274
  .filter((v) => v !== undefined);
491
1275
  rootPkgDependencies.push({
492
1276
  ref,
@@ -496,6 +1280,12 @@ export function parseDumpPackage(dumpJson) {
496
1280
  }
497
1281
  }
498
1282
  metadata.dependencies = rootPkgDependencies;
1283
+ if (rootModules.size) {
1284
+ metadata.rootModules = Array.from(rootModules).sort();
1285
+ }
1286
+ if (testModules.size) {
1287
+ metadata.testModules = Array.from(testModules).sort();
1288
+ }
499
1289
  return metadata;
500
1290
  }
501
1291
 
@@ -504,19 +1294,20 @@ export function parseDumpPackage(dumpJson) {
504
1294
  *
505
1295
  * @param {String} moduleName Module name
506
1296
  * @param {String|Array<string>} compilerArgs Compiler arguments extracted from verbose build log
1297
+ * @param {Object} [env] Environment overrides for sourcekitten
507
1298
  * @returns {undefined|Object} JSON representation of the swift module or undefined.
508
1299
  */
509
- export function moduleInfo(moduleName, compilerArgs) {
1300
+ export function moduleInfo(moduleName, compilerArgs, env) {
510
1301
  const skArgs = ["module-info", "--module", moduleName];
511
1302
  if (compilerArgs && compilerArgs.length > 0) {
512
1303
  skArgs.push("--");
513
1304
  if (Array.isArray(compilerArgs)) {
514
1305
  skArgs.push(...compilerArgs);
515
1306
  } else {
516
- skArgs.push(...compilerArgs.split(" "));
1307
+ skArgs.push(...splitSwiftCommandArgs(compilerArgs));
517
1308
  }
518
1309
  }
519
- return executeSourcekitten(skArgs);
1310
+ return executeSourcekitten(skArgs, env);
520
1311
  }
521
1312
 
522
1313
  /**
@@ -593,12 +1384,24 @@ export function parseModuleInfo(moduleInfoJson) {
593
1384
  }
594
1385
  }
595
1386
  }
596
- // Collect the imported modules
597
- const importedModules = moduleInfoJson?.["key.sourcetext"]
598
- .split("\n")
599
- .filter((l) => l.startsWith("import "))
600
- .map((l) => l.replaceAll("\r", "").replace("import ", ""))
601
- .sort();
1387
+ // Collect the imported modules and the free functions offered by the module
1388
+ const freeFunctions = new Set();
1389
+ const importedModules = [];
1390
+ for (const aline of (moduleInfoJson?.["key.sourcetext"] || "").split("\n")) {
1391
+ const line = aline.replaceAll("\r", "");
1392
+ if (line.startsWith("import ")) {
1393
+ importedModules.push(line.replace("import ", ""));
1394
+ continue;
1395
+ }
1396
+ // Free functions such as Yams' `dump(object:)` are declared at the top
1397
+ // level of the interface; members of types and extensions are indented
1398
+ const funcMatch = line.match(
1399
+ /^(?:@\w+(?:\([^)]*\))?\s+)*(?:public\s+|open\s+)?func\s+([A-Za-z_][A-Za-z0-9_]*)/,
1400
+ );
1401
+ if (funcMatch) {
1402
+ freeFunctions.add(funcMatch[1]);
1403
+ }
1404
+ }
602
1405
  return {
603
1406
  classes: Array.from(classes).sort(),
604
1407
  protocols: Array.from(protocols).sort(),
@@ -606,162 +1409,971 @@ export function parseModuleInfo(moduleInfoJson) {
606
1409
  obfuscationMap,
607
1410
  classMethods,
608
1411
  protocolMethods,
609
- importedModules,
1412
+ functions: Array.from(freeFunctions).sort(),
1413
+ importedModules: importedModules.sort(),
610
1414
  };
611
1415
  }
612
1416
 
613
1417
  /**
614
- * Method to collect the build symbols from the output file maps generated by swift build.
1418
+ * Method to collect the build symbols from the output file maps generated by
1419
+ * swift build.
1420
+ *
1421
+ * Both build engines are supported: the llbuild engine writes per-module
1422
+ * `output-file-map.json` files, while the Swift Build engine (default from
1423
+ * Swift 6.4) writes per-target `<target>-OutputFileMap.json` files under
1424
+ * `.build/out/Intermediates.noindex`.
615
1425
  *
616
1426
  * @param {String} basePath Path
617
1427
  * @param {Object} options CLI options
618
- * @returns {Object} symbols map
1428
+ * @param {Array<String>} [fileMaps] Output file maps, found under basePath when omitted
1429
+ * @returns {Object} symbols map keyed by module name
619
1430
  */
620
- export function collectBuildSymbols(basePath, options) {
621
- const outputFileMaps = getAllFiles(
622
- basePath,
623
- ".build/**/debug/**/output-file-map.json",
624
- options,
625
- );
1431
+ export function collectBuildSymbols(basePath, options, fileMaps) {
626
1432
  const symbolsMap = {};
627
- for (const afilemap of outputFileMaps) {
628
- const metadata = parseOutputFileMap(afilemap);
629
- // Skip testing modules
630
- if (metadata.moduleName.endsWith("Tests")) {
1433
+ const mapFiles =
1434
+ fileMaps ||
1435
+ getAllFiles(
1436
+ basePath,
1437
+ `.build/**/{output-file-map.json,*${SWIFT_BUILD_FILEMAP_SUFFIX}}`,
1438
+ options,
1439
+ );
1440
+ for (const afilemap of mapFiles) {
1441
+ let metadata;
1442
+ try {
1443
+ metadata = parseOutputFileMap(afilemap);
1444
+ } catch (_e) {
1445
+ continue;
1446
+ }
1447
+ if (!metadata?.moduleName || isTestOrAggregateModule(metadata.moduleName)) {
631
1448
  continue;
632
1449
  }
633
- symbolsMap[metadata.moduleName] = metadata.moduleSymbols;
1450
+ const existing = symbolsMap[metadata.moduleName] || [];
1451
+ for (const asym of metadata.moduleSymbols) {
1452
+ if (!existing.includes(asym)) {
1453
+ existing.push(asym);
1454
+ }
1455
+ }
1456
+ symbolsMap[metadata.moduleName] = existing;
634
1457
  }
635
1458
  return symbolsMap;
636
1459
  }
637
1460
 
638
1461
  /**
639
- * Method to parse output file map to identify the module and their symbols.
640
- * This list is imprecise when compared with the data from module-info command.
1462
+ * Method to parse output file map to identify the module, its sources, and
1463
+ * their symbols. The symbol list is imprecise; the sources are exact.
1464
+ *
1465
+ * The module name is derived from the containing directory for llbuild maps
1466
+ * (`<Module>.build` and, since Swift 6.4, `<Module>-tool.build`) and from the
1467
+ * file name for Swift Build maps (`<target>-OutputFileMap.json`, where the
1468
+ * target name still needs to be sanitised into a module name).
641
1469
  *
642
1470
  * @param filemap {String} File name
643
- * @returns {Object} parsed module metadata
1471
+ * @returns {Object} parsed module metadata `{ moduleName, moduleSymbols, sourceFiles }`
644
1472
  */
645
1473
  export function parseOutputFileMap(filemap) {
646
- const moduleName = basename(dirname(filemap)).replace(".build", "");
647
1474
  const fileMapObj = JSON.parse(readFileSync(filemap, { encoding: "utf-8" }));
648
- // Module symbols could be class or protocol names
1475
+ const fileMapName = basename(filemap);
1476
+ let moduleName;
1477
+ if (fileMapName.endsWith(SWIFT_BUILD_FILEMAP_SUFFIX)) {
1478
+ moduleName = toModuleName(
1479
+ fileMapName.slice(0, -SWIFT_BUILD_FILEMAP_SUFFIX.length),
1480
+ );
1481
+ } else {
1482
+ moduleName = basename(dirname(filemap))
1483
+ .replace(/\.build$/, "")
1484
+ .replace(/-tool$/, "");
1485
+ }
649
1486
  const moduleSymbols = [];
1487
+ const sourceFiles = [];
650
1488
  for (const akey of Object.keys(fileMapObj)) {
651
- if (akey.length) {
652
- const symbolName = basename(
653
- fileMapObj[akey]["swift-dependencies"],
654
- ).replace(".swiftdeps", "");
655
- moduleSymbols.push(symbolName.replace("+", "_"));
1489
+ // The empty key describes module-level outputs rather than a source file
1490
+ if (!akey.length) {
1491
+ continue;
1492
+ }
1493
+ if (akey.endsWith(".swift")) {
1494
+ sourceFiles.push(akey);
1495
+ }
1496
+ const swiftDeps = fileMapObj[akey]?.["swift-dependencies"];
1497
+ if (swiftDeps) {
1498
+ moduleSymbols.push(
1499
+ basename(swiftDeps).replace(".swiftdeps", "").replaceAll("+", "_"),
1500
+ );
656
1501
  }
657
1502
  }
658
- return { moduleName, moduleSymbols };
1503
+ return { moduleName, moduleSymbols, sourceFiles };
659
1504
  }
660
1505
 
661
1506
  /**
662
- * Create a precise semantics slices file for a swift project.
1507
+ * Parse `.build/workspace-state.json` into the dependency packages of the
1508
+ * workspace and the directory each one was materialised in.
663
1509
  *
664
- * @param basePath basePath Path
665
- * @param options options CLI options
1510
+ * @param {Object} stateJson Parsed workspace-state.json
1511
+ * @param {String} basePath Root package directory
1512
+ * @returns {Array<Object>} `{ identity, name, location, kind, dir }` per package
666
1513
  */
667
- export function createSemanticsSlices(basePath, options) {
668
- let compilerArgs = process?.env?.SWIFT_COMPILER_ARGS;
669
- let sdkArgs = process?.env?.SWIFT_SDK_ARGS;
670
- const pkgSwiftFiles = getAllFiles(
1514
+ export function parseSwiftWorkspaceState(stateJson, basePath) {
1515
+ const packages = [];
1516
+ const dependencies =
1517
+ stateJson?.object?.dependencies || stateJson?.dependencies || [];
1518
+ for (const adep of dependencies) {
1519
+ const packageRef = adep?.packageRef || {};
1520
+ const identity = packageRef.identity;
1521
+ if (!identity) {
1522
+ continue;
1523
+ }
1524
+ let dir;
1525
+ switch (adep?.state?.name) {
1526
+ case "sourceControlCheckout":
1527
+ case "checkout":
1528
+ dir = join(basePath, ".build", "checkouts", adep.subpath || identity);
1529
+ break;
1530
+ case "registryDownload":
1531
+ dir = join(
1532
+ basePath,
1533
+ ".build",
1534
+ "registry",
1535
+ "downloads",
1536
+ adep.subpath || identity,
1537
+ );
1538
+ break;
1539
+ case "fileSystem":
1540
+ case "local":
1541
+ dir = adep.state?.path || packageRef.location;
1542
+ break;
1543
+ case "edited":
1544
+ dir = adep.state?.path || join(basePath, "Packages", adep.subpath);
1545
+ break;
1546
+ default:
1547
+ dir = undefined;
1548
+ }
1549
+ packages.push({
1550
+ identity,
1551
+ name: packageRef.name || identity,
1552
+ location: packageRef.location,
1553
+ kind: packageRef.kind,
1554
+ dir: dir && isAbsolute(dir) ? dir : undefined,
1555
+ });
1556
+ }
1557
+ return packages;
1558
+ }
1559
+
1560
+ /**
1561
+ * Parse the Swift Build engine's project model (`.build/manifest.pif`) into
1562
+ * the module names each package declares.
1563
+ *
1564
+ * The manifest is a list of workspace, project, and target objects linked
1565
+ * through their `signature` hashes. A project's guid is `PACKAGE:<identity>`
1566
+ * and each target records its `PRODUCT_MODULE_NAME`. Unlike the verbose
1567
+ * build output, the manifest lists modules whose compilation never started,
1568
+ * which keeps the module-to-package mapping complete when a build fails.
1569
+ *
1570
+ * @param {Array} manifestJson Parsed manifest.pif contents
1571
+ * @returns {Object} Map of package identity to module names
1572
+ */
1573
+ export function parseSwiftBuildManifest(manifestJson) {
1574
+ const packageModules = {};
1575
+ if (!Array.isArray(manifestJson)) {
1576
+ return packageModules;
1577
+ }
1578
+ const objectsBySignature = new Map();
1579
+ for (const obj of manifestJson) {
1580
+ if (obj?.signature) {
1581
+ objectsBySignature.set(obj.signature, obj);
1582
+ }
1583
+ }
1584
+ for (const obj of manifestJson) {
1585
+ const guid = obj?.type === "project" ? obj.contents?.guid : undefined;
1586
+ if (!guid?.startsWith("PACKAGE:")) {
1587
+ continue;
1588
+ }
1589
+ const identity = guid.slice("PACKAGE:".length);
1590
+ const modules = new Set();
1591
+ for (const targetSignature of obj.contents.targets || []) {
1592
+ const target = objectsBySignature.get(targetSignature);
1593
+ const contents = target?.type === "target" ? target.contents : undefined;
1594
+ if (!contents?.name || isTestOrAggregateModule(contents.name)) {
1595
+ continue;
1596
+ }
1597
+ for (const buildConfig of contents.buildConfigurations || []) {
1598
+ const moduleName = buildConfig?.buildSettings?.PRODUCT_MODULE_NAME;
1599
+ if (moduleName && !isTestOrAggregateModule(moduleName)) {
1600
+ modules.add(moduleName);
1601
+ break;
1602
+ }
1603
+ }
1604
+ }
1605
+ if (modules.size) {
1606
+ packageModules[identity] = Array.from(modules).sort();
1607
+ }
1608
+ }
1609
+ return packageModules;
1610
+ }
1611
+
1612
+ /**
1613
+ * Blank out the comments and string literals of Swift source, keeping line
1614
+ * breaks so line numbers are unchanged.
1615
+ *
1616
+ * Handles line comments, nested block comments, and single-line, multi-line
1617
+ * (`"""`) and raw (`#"..."#`) string literals.
1618
+ *
1619
+ * @param {String} sourceText Swift source
1620
+ * @returns {String} Source with comments and string contents replaced by spaces
1621
+ */
1622
+ function maskSwiftCommentsAndStrings(sourceText) {
1623
+ const text = sourceText || "";
1624
+ let out = "";
1625
+ let i = 0;
1626
+ const blank = (s) => s.replace(/[^\n]/g, " ");
1627
+ while (i < text.length) {
1628
+ const char = text[i];
1629
+ if (char === "/" && text[i + 1] === "/") {
1630
+ const end = text.indexOf("\n", i);
1631
+ const stop = end === -1 ? text.length : end;
1632
+ out += blank(text.slice(i, stop));
1633
+ i = stop;
1634
+ continue;
1635
+ }
1636
+ if (char === "/" && text[i + 1] === "*") {
1637
+ let depth = 0;
1638
+ let j = i;
1639
+ while (j < text.length) {
1640
+ if (text[j] === "/" && text[j + 1] === "*") {
1641
+ depth++;
1642
+ j += 2;
1643
+ } else if (text[j] === "*" && text[j + 1] === "/") {
1644
+ depth--;
1645
+ j += 2;
1646
+ if (!depth) {
1647
+ break;
1648
+ }
1649
+ } else {
1650
+ j++;
1651
+ }
1652
+ }
1653
+ out += blank(text.slice(i, j));
1654
+ i = j;
1655
+ continue;
1656
+ }
1657
+ if (char === '"' || (char === "#" && /^#+"/.test(text.slice(i, i + 64)))) {
1658
+ let hashes = 0;
1659
+ while (text[i + hashes] === "#") {
1660
+ hashes++;
1661
+ }
1662
+ const quoteStart = i + hashes;
1663
+ const multiline = text.startsWith('"""', quoteStart);
1664
+ const closing = `${multiline ? '"""' : '"'}${"#".repeat(hashes)}`;
1665
+ let j = quoteStart + (multiline ? 3 : 1);
1666
+ while (j < text.length) {
1667
+ // Escapes need as many `#` as the delimiter in raw strings
1668
+ if (
1669
+ text[j] === "\\" &&
1670
+ text.startsWith("#".repeat(hashes), j + 1) &&
1671
+ j + 1 + hashes < text.length
1672
+ ) {
1673
+ j += 2 + hashes;
1674
+ continue;
1675
+ }
1676
+ if (text.startsWith(closing, j)) {
1677
+ j += closing.length;
1678
+ break;
1679
+ }
1680
+ // An unterminated single-line literal ends at the line break
1681
+ if (!multiline && text[j] === "\n") {
1682
+ break;
1683
+ }
1684
+ j++;
1685
+ }
1686
+ out += blank(text.slice(i, j));
1687
+ i = j;
1688
+ continue;
1689
+ }
1690
+ out += char;
1691
+ i++;
1692
+ }
1693
+ return out;
1694
+ }
1695
+
1696
+ /**
1697
+ * Collect the modules imported by a Swift source file, with their lines.
1698
+ *
1699
+ * Declarations such as `@testable import Foo`, `public import Foo`,
1700
+ * `@_implementationOnly import cmark_gfm`, and `import struct Foo.Bar` are
1701
+ * recognised; comments, string literals, and `canImport` conditions are not
1702
+ * imports.
1703
+ *
1704
+ * @param {String} sourceText Swift source
1705
+ * @returns {Array<Object>} `{ module, line }` per import declaration
1706
+ */
1707
+ export function parseSwiftImports(sourceText) {
1708
+ const imports = [];
1709
+ const accessLevels = new Set([
1710
+ "public",
1711
+ "package",
1712
+ "internal",
1713
+ "fileprivate",
1714
+ "private",
1715
+ "open",
1716
+ ]);
1717
+ const importKinds = new Set([
1718
+ "struct",
1719
+ "class",
1720
+ "enum",
1721
+ "protocol",
1722
+ "typealias",
1723
+ "func",
1724
+ "var",
1725
+ "let",
1726
+ ]);
1727
+ const lines = maskSwiftCommentsAndStrings(sourceText).split("\n");
1728
+ for (let i = 0; i < lines.length; i++) {
1729
+ const line = lines[i].trim();
1730
+ if (!line.includes("import")) {
1731
+ continue;
1732
+ }
1733
+ for (const statement of line.split(";")) {
1734
+ const tokens = statement.trim().split(/\s+/);
1735
+ let t = 0;
1736
+ while (
1737
+ t < tokens.length &&
1738
+ (tokens[t].startsWith("@") || accessLevels.has(tokens[t]))
1739
+ ) {
1740
+ t++;
1741
+ }
1742
+ if (tokens[t] !== "import") {
1743
+ continue;
1744
+ }
1745
+ t++;
1746
+ if (importKinds.has(tokens[t])) {
1747
+ t++;
1748
+ }
1749
+ const moduleName = tokens[t]?.split(".")[0];
1750
+ if (moduleName && isIdentifierChar(moduleName[0])) {
1751
+ imports.push({ module: moduleName, line: i + 1 });
1752
+ }
1753
+ }
1754
+ }
1755
+ return imports;
1756
+ }
1757
+
1758
+ /**
1759
+ * Collect the clang module names declared at the top level of a module map.
1760
+ *
1761
+ * @param {String} moduleMapText module.modulemap contents
1762
+ * @returns {Object} `{ modules, headerPaths }`
1763
+ */
1764
+ export function parseClangModuleMap(moduleMapText) {
1765
+ const modules = [];
1766
+ const headerPaths = [];
1767
+ let depth = 0;
1768
+ for (const rawLine of (moduleMapText || "").split("\n")) {
1769
+ const line = rawLine.split("//")[0].trim();
1770
+ if (!line) {
1771
+ continue;
1772
+ }
1773
+ const tokens = line.split(/\s+/);
1774
+ if (depth === 0) {
1775
+ let t = 0;
1776
+ while (["explicit", "framework", "extern"].includes(tokens[t])) {
1777
+ t++;
1778
+ }
1779
+ if (tokens[t] === "module" && tokens[t + 1]) {
1780
+ const moduleName = tokens[t + 1].split("{")[0];
1781
+ if (moduleName && moduleName !== "*") {
1782
+ modules.push(moduleName);
1783
+ }
1784
+ }
1785
+ }
1786
+ const quoteStart = line.indexOf('"');
1787
+ const quoteEnd = line.indexOf('"', quoteStart + 1);
1788
+ if (
1789
+ quoteStart >= 0 &&
1790
+ quoteEnd > quoteStart &&
1791
+ (tokens.includes("header") || tokens.includes("umbrella"))
1792
+ ) {
1793
+ headerPaths.push(line.slice(quoteStart + 1, quoteEnd));
1794
+ }
1795
+ for (const char of line) {
1796
+ if (char === "{") {
1797
+ depth++;
1798
+ } else if (char === "}") {
1799
+ depth = Math.max(0, depth - 1);
1800
+ }
1801
+ }
1802
+ }
1803
+ return { modules, headerPaths };
1804
+ }
1805
+
1806
+ function canonicalPath(filePath) {
1807
+ try {
1808
+ return realpathSync(filePath);
1809
+ } catch (_e) {
1810
+ return filePath;
1811
+ }
1812
+ }
1813
+
1814
+ function isWithinDir(dir, filePath) {
1815
+ if (!dir || !filePath) {
1816
+ return false;
1817
+ }
1818
+ const rel = relative(dir, filePath);
1819
+ return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
1820
+ }
1821
+
1822
+ /**
1823
+ * Attribute a source, module map, or header path to the package that owns
1824
+ * it: the dependency whose directory contains it (the most specific one
1825
+ * wins), the Swift Build intermediates directory named after the package, or
1826
+ * the root package.
1827
+ *
1828
+ * @param {String} filePath Absolute path
1829
+ * @param {Object} workspace `{ basePath, packages }` with canonical directories
1830
+ * @returns {undefined|Object} Owning package, `workspace.root` for the root package
1831
+ */
1832
+ function owningPackage(filePath, workspace) {
1833
+ const target = canonicalPath(filePath);
1834
+ let best;
1835
+ for (const apkg of workspace.packages) {
1836
+ if (
1837
+ apkg.canonicalDir &&
1838
+ isWithinDir(apkg.canonicalDir, target) &&
1839
+ (!best || apkg.canonicalDir.length > best.canonicalDir.length)
1840
+ ) {
1841
+ best = apkg;
1842
+ }
1843
+ }
1844
+ if (best) {
1845
+ return best;
1846
+ }
1847
+ // Swift Build keeps generated module maps in
1848
+ // `.build/out/Intermediates.noindex/<package identity>.build/`
1849
+ const segments = target.split(PATH_SEPARATORS);
1850
+ const intermediates = segments.indexOf("Intermediates.noindex");
1851
+ if (intermediates >= 0 && segments[intermediates + 1]?.endsWith(".build")) {
1852
+ const identity = segments[intermediates + 1].slice(0, -".build".length);
1853
+ const byIdentity = workspace.packages.find(
1854
+ (p) => p.identity === identity.toLowerCase() || p.name === identity,
1855
+ );
1856
+ if (byIdentity) {
1857
+ return byIdentity;
1858
+ }
1859
+ if (identity === workspace.root.name) {
1860
+ return workspace.root;
1861
+ }
1862
+ return undefined;
1863
+ }
1864
+ if (isWithinDir(workspace.canonicalBasePath, target)) {
1865
+ const rel = relative(workspace.canonicalBasePath, target);
1866
+ return rel.split(PATH_SEPARATORS)[0] === ".build"
1867
+ ? undefined
1868
+ : workspace.root;
1869
+ }
1870
+ return undefined;
1871
+ }
1872
+
1873
+ /**
1874
+ * Build a description of every module in a completed swift build: its
1875
+ * compiler arguments, sources, and owning package.
1876
+ *
1877
+ * Arguments come from the llbuild build description when present and from
1878
+ * the verbose build output otherwise (the only source for the Swift Build
1879
+ * engine); sources come from the same places or from the output file maps.
1880
+ * Packages come from `workspace-state.json`, and every module is attributed
1881
+ * to the package whose directory holds its sources. Clang modules are
1882
+ * discovered through their module maps.
1883
+ *
1884
+ * @param {String} basePath Root package directory
1885
+ * @param {String} buildOutput Verbose build output
1886
+ * @param {Object} options CLI options
1887
+ * @param {Object} [rootMetadata] Parsed dump-package of the root package
1888
+ * @returns {Object} `{ modules, packages, root, clangModules, fileMaps }`
1889
+ */
1890
+ export function collectSwiftBuildPlan(
1891
+ basePath,
1892
+ buildOutput,
1893
+ options,
1894
+ rootMetadata,
1895
+ ) {
1896
+ const buildArtifacts = getAllFiles(
671
1897
  basePath,
672
- `${options.multiProject ? "**/" : ""}Package*.swift`,
1898
+ `.build/**/{description.json,output-file-map.json,*${SWIFT_BUILD_FILEMAP_SUFFIX},module.modulemap}`,
673
1899
  options,
674
1900
  );
675
- if (!pkgSwiftFiles.length) {
676
- return undefined;
677
- }
678
- const hasSwiftVersionFile = basePath
679
- ? safeExistsSync(join(basePath, ".swift-version"))
680
- : false;
681
- if (!compilerArgs || !sdkArgs) {
682
- // We begin by performing a clean verbose debug build to learn the compiler arguments needed for a successful build
683
- // We do this because most users would not know the compiler arguments themselves!
684
- const paramsObj = extractCompilerParamsFromBuild(verboseBuild(basePath));
685
- // Our auto-detection attempt has failed.
686
- if (!paramsObj) {
687
- if (readEnvironmentVariable("CDXGEN_IN_CONTAINER") !== "true") {
688
- console.log(
689
- "Automatic swift build has failed. Check if the appropriate version of swift is installed. Try using the cdxgen container image, which bundles the latest Swift 6 compiler.",
690
- );
691
- } else {
692
- console.log(
693
- "Automatic swift build has failed. Check if this project is compatible with Swift 5/6 that is bundled with the cdxgen container image.",
1901
+ const fileMaps = [];
1902
+ const moduleMapFiles = new Set();
1903
+ const modules = {};
1904
+ const seenDescriptions = new Set();
1905
+ for (const artifact of buildArtifacts) {
1906
+ const name = basename(artifact);
1907
+ // Dependency repositories may ship build files of their own as test
1908
+ // fixtures; only their module maps describe real clang modules
1909
+ const buildSubdir = relative(join(basePath, ".build"), artifact).split(
1910
+ PATH_SEPARATORS,
1911
+ )[0];
1912
+ if (
1913
+ name !== "module.modulemap" &&
1914
+ ["checkouts", "repositories", "registry"].includes(buildSubdir)
1915
+ ) {
1916
+ continue;
1917
+ }
1918
+ if (name === "description.json") {
1919
+ const realDescription = canonicalPath(artifact);
1920
+ if (
1921
+ seenDescriptions.has(realDescription) ||
1922
+ !artifact.toLowerCase().split(PATH_SEPARATORS).includes("debug")
1923
+ ) {
1924
+ continue;
1925
+ }
1926
+ seenDescriptions.add(realDescription);
1927
+ try {
1928
+ Object.assign(
1929
+ modules,
1930
+ parseSwiftBuildDescription(
1931
+ JSON.parse(readFileSync(artifact, { encoding: "utf-8" })),
1932
+ ),
694
1933
  );
1934
+ } catch (_e) {
1935
+ // Fall back to the verbose build output
695
1936
  }
696
- return;
1937
+ } else if (name === "module.modulemap") {
1938
+ moduleMapFiles.add(artifact);
1939
+ } else {
1940
+ fileMaps.push(artifact);
697
1941
  }
698
- compilerArgs = paramsObj.compilerArgs;
699
- if (paramsObj?.params?.["-sdk"]) {
700
- sdkArgs = Array.from(paramsObj.params["-sdk"]).join(" ");
1942
+ }
1943
+ for (const [moduleName, invocation] of Object.entries(
1944
+ parseSwiftModuleInvocations(buildOutput),
1945
+ )) {
1946
+ if (!modules[moduleName]) {
1947
+ modules[moduleName] = invocation;
1948
+ } else if (!modules[moduleName].sources.length) {
1949
+ modules[moduleName].sources = invocation.sources;
701
1950
  }
702
- if (DEBUG_MODE && !sdkArgs && !hasSwiftVersionFile) {
703
- console.log(
704
- "TIP: Unable to detect the swift sdk needed to build this project. Try running the swift build command to check if this project builds successfully.",
1951
+ }
1952
+ // Output file maps list the exact sources of every module
1953
+ for (const afilemap of fileMaps) {
1954
+ let metadata;
1955
+ try {
1956
+ metadata = parseOutputFileMap(afilemap);
1957
+ } catch (_e) {
1958
+ continue;
1959
+ }
1960
+ if (!metadata?.moduleName || !metadata.sourceFiles.length) {
1961
+ continue;
1962
+ }
1963
+ const amodule = modules[metadata.moduleName];
1964
+ if (!amodule) {
1965
+ modules[metadata.moduleName] = {
1966
+ args: [],
1967
+ sources: metadata.sourceFiles,
1968
+ isDriver: false,
1969
+ };
1970
+ } else if (!amodule.sources.length) {
1971
+ amodule.sources = metadata.sourceFiles;
1972
+ }
1973
+ }
1974
+ const rootName = rootMetadata?.rootModule || basename(basePath);
1975
+ const workspace = {
1976
+ basePath,
1977
+ canonicalBasePath: canonicalPath(basePath),
1978
+ root: { identity: rootName.toLowerCase(), name: rootName, root: true },
1979
+ packages: [],
1980
+ };
1981
+ const workspaceStateFile = join(basePath, ".build", "workspace-state.json");
1982
+ if (safeExistsSync(workspaceStateFile)) {
1983
+ try {
1984
+ workspace.packages = parseSwiftWorkspaceState(
1985
+ JSON.parse(readFileSync(workspaceStateFile, { encoding: "utf-8" })),
1986
+ basePath,
705
1987
  );
706
- console.log(
707
- "Check whether the project requires xcodebuild to build. Such projects are currently unsupported.",
1988
+ } catch (_e) {
1989
+ workspace.packages = [];
1990
+ }
1991
+ }
1992
+ for (const apkg of workspace.packages) {
1993
+ apkg.canonicalDir = apkg.dir ? canonicalPath(apkg.dir) : undefined;
1994
+ apkg.modules = new Set();
1995
+ }
1996
+ workspace.root.modules = new Set();
1997
+ const testModules = new Set(rootMetadata?.testModules || []);
1998
+ for (const [moduleName, amodule] of Object.entries(modules)) {
1999
+ const firstSource =
2000
+ amodule.sources.find(
2001
+ (s) => !s.split(PATH_SEPARATORS).includes(".build"),
2002
+ ) || amodule.sources[0];
2003
+ const owner = firstSource
2004
+ ? owningPackage(firstSource, workspace)
2005
+ : undefined;
2006
+ amodule.package = owner?.root ? workspace.root.identity : owner?.identity;
2007
+ amodule.isRoot = Boolean(owner?.root);
2008
+ // Local packages kept inside the scanned directory (monorepos) are part
2009
+ // of the project's own code
2010
+ amodule.isWorkspace =
2011
+ amodule.isRoot ||
2012
+ Boolean(
2013
+ owner?.kind === "fileSystem" &&
2014
+ owner.canonicalDir &&
2015
+ isWithinDir(workspace.canonicalBasePath, owner.canonicalDir),
708
2016
  );
2017
+ amodule.isTest =
2018
+ testModules.has(moduleName) ||
2019
+ isTestOrAggregateModule(moduleName) ||
2020
+ (amodule.sources.length > 0 &&
2021
+ amodule.sources.every((s) =>
2022
+ relative(basePath, s).split(PATH_SEPARATORS).includes("Tests"),
2023
+ ));
2024
+ owner?.modules.add(moduleName);
2025
+ // Module maps referenced through -fmodule-map-file describe clang modules
2026
+ for (let i = 0; i < amodule.args.length; i++) {
2027
+ if (
2028
+ amodule.args[i] === "-Xcc" &&
2029
+ amodule.args[i + 1]?.startsWith("-fmodule-map-file=")
2030
+ ) {
2031
+ moduleMapFiles.add(
2032
+ amodule.args[i + 1].slice("-fmodule-map-file=".length),
2033
+ );
2034
+ }
709
2035
  }
710
2036
  }
711
- // Success we now have the compiler and sdk arguments
712
- if (DEBUG_MODE) {
713
- console.log("Detected swift compiler arguments", compilerArgs);
2037
+ // The Swift Build project model names the modules of every package, also
2038
+ // those a failed build never compiled
2039
+ const manifestFile = join(basePath, ".build", "manifest.pif");
2040
+ if (safeExistsSync(manifestFile)) {
2041
+ let manifestModules = {};
2042
+ try {
2043
+ manifestModules = parseSwiftBuildManifest(
2044
+ JSON.parse(readFileSync(manifestFile, { encoding: "utf-8" })),
2045
+ );
2046
+ } catch (_e) {
2047
+ manifestModules = {};
2048
+ }
2049
+ for (const [identity, moduleNames] of Object.entries(manifestModules)) {
2050
+ const owner =
2051
+ workspace.packages.find((p) => p.identity === identity) ||
2052
+ workspace.root;
2053
+ for (const moduleName of moduleNames) {
2054
+ const attributed =
2055
+ modules[moduleName]?.package !== undefined ||
2056
+ [workspace.root, ...workspace.packages].some((p) =>
2057
+ p.modules.has(moduleName),
2058
+ );
2059
+ if (!attributed) {
2060
+ owner.modules.add(moduleName);
2061
+ }
2062
+ }
2063
+ }
714
2064
  }
715
- // Package.swift file contains useful information needed to understand the semantic context
716
- // Let's use the dump-package command to retriev this information in json format
717
- const packageMetadata = parseDumpPackage(dumpPackage(basePath));
718
- // Our attempt to build must have yielded some output file maps.
719
- // These can be used to understand the symbols offered by each of the dependency
720
- const buildSymbols = collectBuildSymbols(basePath, options);
721
- // Now let's attempt to learn about each module (internal and external) in detail
722
- // Information about the classes, protocols, enums, and methods exported by each module is valuable
723
- const moduleInfos = {};
724
- const allModules = new Set([
725
- packageMetadata?.rootModule.trim(),
726
- ...(packageMetadata?.dependencies || []).map((d) => d.ref.trim()),
727
- ...Object.keys(buildSymbols),
728
- ]);
729
- for (const moduleName of Array.from(allModules)) {
730
- // Skip the testing modules
731
- if (
732
- !moduleName ||
733
- moduleName?.length === 0 ||
734
- moduleName?.endsWith("Tests")
735
- ) {
2065
+ const clangModules = {};
2066
+ for (const moduleMapFile of moduleMapFiles) {
2067
+ if (!safeExistsSync(moduleMapFile)) {
736
2068
  continue;
737
2069
  }
738
- const moduleInfoObj = parseModuleInfo(moduleInfo(moduleName, compilerArgs));
739
- if (moduleInfoObj) {
740
- moduleInfos[moduleName] = moduleInfoObj;
741
- } else {
742
- console.log(
743
- "Unable to obtain the semantic context for the module",
744
- moduleName,
2070
+ let parsed;
2071
+ try {
2072
+ parsed = parseClangModuleMap(
2073
+ readFileSync(moduleMapFile, { encoding: "utf-8" }),
745
2074
  );
2075
+ } catch (_e) {
2076
+ continue;
2077
+ }
2078
+ // Generated module maps live in the build directory; their headers point
2079
+ // back at the package sources
2080
+ const attributionPath = moduleMapFile
2081
+ .split(PATH_SEPARATORS)
2082
+ .includes(".build")
2083
+ ? parsed.headerPaths.find((h) => isAbsolute(h)) || moduleMapFile
2084
+ : moduleMapFile;
2085
+ const owner = owningPackage(attributionPath, workspace);
2086
+ if (!owner) {
2087
+ continue;
2088
+ }
2089
+ for (const clangModule of parsed.modules) {
2090
+ if (!modules[clangModule] && !clangModules[clangModule]) {
2091
+ clangModules[clangModule] = owner.root
2092
+ ? workspace.root.identity
2093
+ : owner.identity;
2094
+ owner.modules.add(clangModule);
2095
+ }
2096
+ }
2097
+ }
2098
+ return {
2099
+ modules,
2100
+ clangModules,
2101
+ root: workspace.root,
2102
+ packages: workspace.packages,
2103
+ fileMaps,
2104
+ };
2105
+ }
2106
+
2107
+ /**
2108
+ * Register a package's modules under its identity, name, and repository name
2109
+ * (and their lowercase forms) so that SBOM components can be matched
2110
+ * regardless of how they were named.
2111
+ *
2112
+ * @param {Object} packageModules Map to mutate
2113
+ * @param {Object} apkg Package with `identity`, `name`, `location`, `modules`
2114
+ */
2115
+ function addPackageModuleAliases(packageModules, apkg) {
2116
+ const modules = Array.from(apkg.modules || []).sort();
2117
+ if (!modules.length) {
2118
+ return;
2119
+ }
2120
+ const repoName = (apkg.location || "")
2121
+ .split("?")[0]
2122
+ .split("#")[0]
2123
+ .replace(/\/+$/, "")
2124
+ .split("/")
2125
+ .pop()
2126
+ ?.replace(/\.git$/, "");
2127
+ for (const alias of [apkg.identity, apkg.name, repoName]) {
2128
+ for (const key of [alias, alias?.toLowerCase()]) {
2129
+ if (key) {
2130
+ packageModules[key] = Array.from(
2131
+ new Set([...(packageModules[key] || []), ...modules]),
2132
+ ).sort();
2133
+ }
746
2134
  }
747
2135
  }
748
- // Finally, let's do some structural analysis of swift source codes
749
- const swiftFiles = getAllFiles(basePath, "**/*.swift", options);
750
- const fileStructures = {};
2136
+ }
2137
+
2138
+ /**
2139
+ * Check that sourcekitten can load SourceKit, with a trivial syntax request.
2140
+ *
2141
+ * A missing binary, a binary without its Swift runtime (`error while loading
2142
+ * shared libraries: libswiftCore.so`), or one linked against a different
2143
+ * Swift runtime than the toolchain provides (Linux has no stable Swift ABI, so
2144
+ * such a binary crashes inside libswiftCore) fails every request; the probe
2145
+ * turns that into one clear message instead of silent empty results.
2146
+ *
2147
+ * @param {Object} [env] Environment overrides for sourcekitten
2148
+ * @returns {boolean} `true` when sourcekitten answered the probe
2149
+ */
2150
+ export function isSourcekittenUsable(env) {
2151
+ const status = probeSourcekitten(env);
2152
+ if (status === "ok") {
2153
+ return true;
2154
+ }
2155
+ if (status === "unavailable") {
2156
+ // cdxgen-plugins-bin ships sourcekitten for macOS only: a prebuilt Linux
2157
+ // binary runs only next to the Swift toolchain that built it.
2158
+ console.log(
2159
+ process.platform === "darwin"
2160
+ ? "sourcekitten is unavailable, so Swift evidence is limited to import declarations. Install the cdxgen-plugins-bin package for this platform or set SOURCEKITTEN_CMD."
2161
+ : "sourcekitten is unavailable, so Swift evidence is limited to import declarations. Build sourcekitten with the Swift toolchain used here and set SOURCEKITTEN_CMD, or use a cdxgen container image that includes Swift.",
2162
+ );
2163
+ return false;
2164
+ }
2165
+ if (status === "not-found") {
2166
+ console.log(
2167
+ readEnvironmentVariable("SOURCEKITTEN_CMD")
2168
+ ? "SOURCEKITTEN_CMD does not name an executable that exists, so Swift evidence is limited to import declarations. Point it at a sourcekitten binary."
2169
+ : "The bundled sourcekitten executable is missing, so Swift evidence is limited to import declarations. Reinstall the cdxgen-plugins-bin package for this platform or set SOURCEKITTEN_CMD.",
2170
+ );
2171
+ return false;
2172
+ }
2173
+ console.log(
2174
+ "sourcekitten could not load SourceKit, so Swift evidence is limited to import declarations. On Linux, sourcekitten must be linked against the Swift runtime of this toolchain; set SOURCEKITTEN_CMD to a sourcekitten built with it (or with --static-swift-stdlib). Set CDXGEN_DEBUG_MODE=debug to see the error.",
2175
+ );
2176
+ return false;
2177
+ }
2178
+
2179
+ /**
2180
+ * Create a precise semantics slices file for a swift project.
2181
+ *
2182
+ * The project is built once in verbose mode. Every source file of the root
2183
+ * package's (non-test) modules is then indexed with sourcekitten using that
2184
+ * module's own compiler arguments and source list, with sourcekitten aligned
2185
+ * to the toolchain that compiled the project. The declaring module of every
2186
+ * resolved reference is recovered from its USR, which attributes each usage
2187
+ * to the package that provides the module (`moduleReferences`).
2188
+ *
2189
+ * @param basePath basePath Path
2190
+ * @param options options CLI options
2191
+ */
2192
+ export function createSemanticsSlices(basePath, options) {
2193
+ const pkgSwiftFiles = getAllFiles(
2194
+ basePath,
2195
+ `${options.multiProject ? "**/" : ""}Package*.swift`,
2196
+ options,
2197
+ );
2198
+ if (!pkgSwiftFiles.length) {
2199
+ return undefined;
2200
+ }
2201
+ const toolchain = resolveSwiftToolchain(basePath);
2202
+ const sourcekittenEnv = sourcekittenEnvForToolchain(toolchain);
2203
+ // Without a working sourcekitten, the evidence is limited to the import
2204
+ // declarations of the modules the build describes
2205
+ const useSourcekitten = isSourcekittenUsable(sourcekittenEnv);
2206
+ if (DEBUG_MODE && toolchain) {
2207
+ console.log(
2208
+ `Swift toolchain for ${basePath}: ${toolchain.compilerVersion} at ${toolchain.toolchainDir}.`,
2209
+ sourcekittenEnv
2210
+ ? `sourcekitten environment: ${JSON.stringify(sourcekittenEnv)}`
2211
+ : "",
2212
+ );
2213
+ }
2214
+ // Package.swift contains the root targets, including which ones are tests
2215
+ const packageMetadata = parseDumpPackage(dumpPackage(basePath));
2216
+ const userCompilerArgs = process?.env?.SWIFT_COMPILER_ARGS;
2217
+ const userSdkArgs = process?.env?.SWIFT_SDK_ARGS;
2218
+ // A clean verbose debug build reveals the compiler arguments of every module
2219
+ const buildOutput =
2220
+ userCompilerArgs && userSdkArgs ? undefined : verboseBuild(basePath);
2221
+ const buildPlan = collectSwiftBuildPlan(
2222
+ basePath,
2223
+ buildOutput,
2224
+ options,
2225
+ packageMetadata,
2226
+ );
2227
+ const packageModules = {};
2228
+ for (const apkg of [buildPlan.root, ...buildPlan.packages]) {
2229
+ addPackageModuleAliases(packageModules, apkg);
2230
+ }
2231
+ const packages = buildPlan.packages
2232
+ .filter((p) => p.modules.size)
2233
+ .map((p) => ({
2234
+ identity: p.identity,
2235
+ name: p.name,
2236
+ location: p.location,
2237
+ kind: p.kind,
2238
+ modules: Array.from(p.modules).sort(),
2239
+ }));
2240
+ const dependencyModules = new Set(
2241
+ buildPlan.packages.flatMap((p) => Array.from(p.modules)),
2242
+ );
2243
+ const rootModuleNames = Object.keys(buildPlan.modules).filter(
2244
+ (m) => buildPlan.modules[m].isWorkspace && !buildPlan.modules[m].isTest,
2245
+ );
2246
+ // A private module cache per run: the build's cache is shared with every
2247
+ // compiler that ever touched the project, and modules from a mismatched
2248
+ // toolchain make SourceKit return empty results
2249
+ const moduleCachePath = safeMkdtempSync(join(getTmpDir(), "swiftsem-cache-"));
751
2250
  const fileIndexes = {};
752
- for (const afile of swiftFiles) {
753
- // Skip testing and Package.swift
754
- if (afile.includes("Tests") || afile.endsWith("Package.swift")) {
755
- continue;
2251
+ const indexedFiles = new Set();
2252
+ const indexFile = (afile, compilerArgs) => {
2253
+ fileIndexes[afile] = useSourcekitten
2254
+ ? parseIndex(index(afile, compilerArgs, sourcekittenEnv))
2255
+ : undefined;
2256
+ indexedFiles.add(canonicalPath(afile));
2257
+ };
2258
+ const isIndexableSource = (afile) =>
2259
+ afile.endsWith(".swift") &&
2260
+ !/^Package(@.+)?\.swift$/.test(basename(afile)) &&
2261
+ !relative(basePath, afile).split(PATH_SEPARATORS).includes(".build") &&
2262
+ safeExistsSync(afile);
2263
+ try {
2264
+ const moduleArgs = {};
2265
+ for (const moduleName of rootModuleNames) {
2266
+ const amodule = buildPlan.modules[moduleName];
2267
+ moduleArgs[moduleName] = [
2268
+ ...(userCompilerArgs
2269
+ ? splitSwiftCommandArgs(`${userCompilerArgs} ${userSdkArgs || ""}`)
2270
+ : amodule.args),
2271
+ "-module-cache-path",
2272
+ moduleCachePath,
2273
+ ];
2274
+ if (!moduleArgs[moduleName].includes("-module-name")) {
2275
+ moduleArgs[moduleName].push("-module-name", moduleName);
2276
+ }
2277
+ // Every source of the module is passed so that references to
2278
+ // declarations in the module's other files (including generated
2279
+ // sources) resolve; only the project's own files are indexed
2280
+ const moduleSources = amodule.sources.filter((s) => safeExistsSync(s));
2281
+ for (const afile of moduleSources.filter(isIndexableSource)) {
2282
+ indexFile(afile, [...moduleArgs[moduleName], ...moduleSources]);
2283
+ }
2284
+ }
2285
+ // Sources that belong to no known module, for example when the build
2286
+ // failed, are indexed with the merged arguments of the root modules
2287
+ const fallbackArgs = userCompilerArgs
2288
+ ? splitSwiftCommandArgs(`${userCompilerArgs} ${userSdkArgs || ""}`)
2289
+ : extractCompilerParamsFromBuild(
2290
+ buildOutput,
2291
+ packageMetadata?.rootModules,
2292
+ ).compilerArgs;
2293
+ // Sources of test and dependency modules are never indexed
2294
+ const plannedSources = new Set();
2295
+ for (const amodule of Object.values(buildPlan.modules)) {
2296
+ for (const asource of amodule.sources) {
2297
+ plannedSources.add(canonicalPath(asource));
2298
+ }
2299
+ }
2300
+ for (const afile of getAllFiles(basePath, "**/*.swift", options)) {
2301
+ const canonicalFile = canonicalPath(afile);
2302
+ if (
2303
+ indexedFiles.has(canonicalFile) ||
2304
+ plannedSources.has(canonicalFile) ||
2305
+ !isIndexableSource(afile) ||
2306
+ relative(basePath, afile)
2307
+ .split(PATH_SEPARATORS)
2308
+ .some((segment) => segment.endsWith("Tests"))
2309
+ ) {
2310
+ continue;
2311
+ }
2312
+ indexFile(afile, [
2313
+ ...fallbackArgs,
2314
+ "-module-cache-path",
2315
+ moduleCachePath,
2316
+ ]);
2317
+ }
2318
+ // Attribute every resolved reference to its declaring module
2319
+ const allUsrs = new Set();
2320
+ for (const afileIndex of Object.values(fileIndexes)) {
2321
+ for (const ausr of Object.keys(afileIndex?.usrLines || {})) {
2322
+ allUsrs.add(ausr);
2323
+ }
2324
+ }
2325
+ const usrModules = resolveUsrModules(Array.from(allUsrs), basePath);
2326
+ for (const afile of Object.keys(fileIndexes)) {
2327
+ const afileIndex = fileIndexes[afile] || {};
2328
+ const moduleLines = {};
2329
+ const addLines = (moduleName, lines) => {
2330
+ if (!dependencyModules.has(moduleName)) {
2331
+ return;
2332
+ }
2333
+ if (!moduleLines[moduleName]) {
2334
+ moduleLines[moduleName] = new Set();
2335
+ }
2336
+ for (const aline of lines) {
2337
+ moduleLines[moduleName].add(aline);
2338
+ }
2339
+ };
2340
+ for (const [ausr, lines] of Object.entries(afileIndex.usrLines || {})) {
2341
+ const moduleName = usrModules.get(ausr);
2342
+ if (moduleName) {
2343
+ addLines(moduleName, lines);
2344
+ }
2345
+ }
2346
+ // Import declarations are evidence even when SourceKit could not load
2347
+ // the module, for example after a partial build
2348
+ let sourceText;
2349
+ try {
2350
+ sourceText = readFileSync(afile, { encoding: "utf-8" });
2351
+ } catch (_e) {
2352
+ sourceText = "";
2353
+ }
2354
+ for (const aimport of parseSwiftImports(sourceText)) {
2355
+ addLines(aimport.module, [aimport.line]);
2356
+ }
2357
+ const moduleReferences = {};
2358
+ for (const moduleName of Object.keys(moduleLines).sort()) {
2359
+ moduleReferences[moduleName] = Array.from(moduleLines[moduleName]).sort(
2360
+ (a, b) => a - b,
2361
+ );
2362
+ }
2363
+ afileIndex.moduleReferences = moduleReferences;
2364
+ delete afileIndex.usrLines;
2365
+ fileIndexes[afile] = afileIndex;
756
2366
  }
757
- fileStructures[afile] = parseStructure(getStructure(afile));
758
- fileIndexes[afile] = parseIndex(index(afile, compilerArgs));
2367
+ } finally {
2368
+ safeRmSync(moduleCachePath, { recursive: true, force: true });
759
2369
  }
760
2370
  return {
2371
+ projectPath: basePath,
2372
+ toolchain: toolchain?.compilerVersion,
761
2373
  packageMetadata,
762
- buildSymbols,
763
- moduleInfos,
764
- fileStructures,
2374
+ packages,
2375
+ packageModules,
2376
+ buildSymbols: collectBuildSymbols(basePath, options, buildPlan.fileMaps),
765
2377
  fileIndexes,
766
2378
  };
767
2379
  }