@hominis/fireforge 0.44.4 → 0.46.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 (378) hide show
  1. package/CHANGELOG.md +120 -0
  2. package/dist/bin/fireforge.js +6 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/cli-usage-error.d.ts +46 -0
  5. package/dist/src/cli-usage-error.js +81 -0
  6. package/dist/src/cli.d.ts +5 -1
  7. package/dist/src/cli.js +20 -21
  8. package/dist/src/commands/bootstrap.js +1 -2
  9. package/dist/src/commands/build.js +15 -6
  10. package/dist/src/commands/destructive-decision.d.ts +19 -0
  11. package/dist/src/commands/destructive-decision.js +31 -0
  12. package/dist/src/commands/doctor-check-core.d.ts +0 -6
  13. package/dist/src/commands/doctor-check-core.js +0 -8
  14. package/dist/src/commands/doctor-orphaned-harness.d.ts +0 -6
  15. package/dist/src/commands/doctor-orphaned-harness.js +4 -12
  16. package/dist/src/commands/doctor-post-rebase-audit.js +1 -1
  17. package/dist/src/commands/doctor-source-pin.d.ts +29 -0
  18. package/dist/src/commands/doctor-source-pin.js +81 -0
  19. package/dist/src/commands/doctor.d.ts +2 -0
  20. package/dist/src/commands/doctor.js +9 -8
  21. package/dist/src/commands/download.js +35 -24
  22. package/dist/src/commands/export-all.js +21 -4
  23. package/dist/src/commands/export-flow.js +15 -60
  24. package/dist/src/commands/export-placement-conflicts.js +3 -3
  25. package/dist/src/commands/export-placement-gate.js +2 -8
  26. package/dist/src/commands/export-placement-policy.js +3 -5
  27. package/dist/src/commands/export-shared.d.ts +42 -23
  28. package/dist/src/commands/export-shared.js +4 -19
  29. package/dist/src/commands/export.js +10 -1
  30. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  31. package/dist/src/commands/furnace/authoring-config.js +19 -0
  32. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  33. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  34. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  35. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  36. package/dist/src/commands/furnace/create-templates.js +2 -1
  37. package/dist/src/commands/furnace/create.js +25 -19
  38. package/dist/src/commands/furnace/deploy.js +11 -2
  39. package/dist/src/commands/furnace/init.js +2 -1
  40. package/dist/src/commands/furnace/override.js +28 -19
  41. package/dist/src/commands/furnace/refresh.js +9 -1
  42. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  43. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  44. package/dist/src/commands/furnace/remove.js +2 -6
  45. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  46. package/dist/src/commands/furnace/rename.js +29 -29
  47. package/dist/src/commands/furnace/status.js +12 -9
  48. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  49. package/dist/src/commands/furnace/test-file-name.js +39 -0
  50. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  51. package/dist/src/commands/furnace/validation-output.js +3 -8
  52. package/dist/src/commands/import.js +2 -9
  53. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  54. package/dist/src/commands/lint-per-patch.js +19 -4
  55. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  56. package/dist/src/commands/lint.d.ts +2 -1
  57. package/dist/src/commands/lint.js +1 -1
  58. package/dist/src/commands/manifest.js +1 -1
  59. package/dist/src/commands/package.js +2 -4
  60. package/dist/src/commands/patch/compact.js +14 -15
  61. package/dist/src/commands/patch/delete.js +15 -25
  62. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  63. package/dist/src/commands/patch/entry-projection.js +24 -0
  64. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  65. package/dist/src/commands/patch/lint-ignore.js +19 -29
  66. package/dist/src/commands/patch/move-files-create.js +24 -10
  67. package/dist/src/commands/patch/move-files-into.js +38 -37
  68. package/dist/src/commands/patch/move-files.js +4 -13
  69. package/dist/src/commands/patch/rename.js +27 -41
  70. package/dist/src/commands/patch/reorder.js +25 -38
  71. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  72. package/dist/src/commands/patch/split-plan.js +4 -11
  73. package/dist/src/commands/patch/split.js +45 -43
  74. package/dist/src/commands/patch/staged-dependency.js +16 -22
  75. package/dist/src/commands/patch/tier.js +15 -25
  76. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  77. package/dist/src/commands/patch-rename-map.js +36 -0
  78. package/dist/src/commands/pending-resolution.d.ts +15 -0
  79. package/dist/src/commands/pending-resolution.js +25 -0
  80. package/dist/src/commands/re-export-adjacent.d.ts +32 -2
  81. package/dist/src/commands/re-export-adjacent.js +51 -10
  82. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  83. package/dist/src/commands/re-export-drift.d.ts +8 -0
  84. package/dist/src/commands/re-export-drift.js +65 -4
  85. package/dist/src/commands/re-export-files.d.ts +2 -3
  86. package/dist/src/commands/re-export-files.js +38 -32
  87. package/dist/src/commands/re-export-lint.d.ts +3 -4
  88. package/dist/src/commands/re-export-lint.js +9 -2
  89. package/dist/src/commands/re-export-options.js +19 -5
  90. package/dist/src/commands/re-export-register.js +5 -1
  91. package/dist/src/commands/re-export-scan.js +8 -8
  92. package/dist/src/commands/re-export-single.d.ts +22 -3
  93. package/dist/src/commands/re-export-single.js +19 -11
  94. package/dist/src/commands/re-export.js +44 -27
  95. package/dist/src/commands/rebase/abort.js +3 -8
  96. package/dist/src/commands/rebase/continue.js +12 -12
  97. package/dist/src/commands/rebase/index.js +2 -2
  98. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  99. package/dist/src/commands/rebase/patch-loop.js +2 -7
  100. package/dist/src/commands/resolve.js +18 -10
  101. package/dist/src/commands/run.d.ts +0 -13
  102. package/dist/src/commands/run.js +12 -25
  103. package/dist/src/commands/setup-support.d.ts +16 -4
  104. package/dist/src/commands/setup-support.js +16 -16
  105. package/dist/src/commands/setup.js +3 -9
  106. package/dist/src/commands/source.js +8 -1
  107. package/dist/src/commands/status-check.js +2 -2
  108. package/dist/src/commands/status-output.js +2 -2
  109. package/dist/src/commands/status.js +3 -4
  110. package/dist/src/commands/test-build-phase.d.ts +2 -2
  111. package/dist/src/commands/test-build-phase.js +33 -8
  112. package/dist/src/commands/test-diagnose.js +14 -5
  113. package/dist/src/commands/test-modes.d.ts +2 -2
  114. package/dist/src/commands/test-modes.js +0 -1
  115. package/dist/src/commands/test-register.js +6 -3
  116. package/dist/src/commands/test-run.js +29 -21
  117. package/dist/src/commands/test-stale-gate.js +8 -5
  118. package/dist/src/commands/test-verdict.d.ts +16 -2
  119. package/dist/src/commands/test-verdict.js +18 -3
  120. package/dist/src/commands/test.js +51 -7
  121. package/dist/src/commands/token-list.d.ts +26 -0
  122. package/dist/src/commands/token-list.js +153 -0
  123. package/dist/src/commands/token.js +17 -1
  124. package/dist/src/commands/tree.js +7 -2
  125. package/dist/src/commands/typecheck.js +9 -9
  126. package/dist/src/commands/wire.js +4 -3
  127. package/dist/src/core/branding.d.ts +0 -7
  128. package/dist/src/core/branding.js +3 -2
  129. package/dist/src/core/browser-wire.js +6 -3
  130. package/dist/src/core/build-audit-platform.d.ts +29 -0
  131. package/dist/src/core/build-audit-platform.js +140 -0
  132. package/dist/src/core/build-audit-registration.js +3 -2
  133. package/dist/src/core/build-audit-skip.d.ts +57 -0
  134. package/dist/src/core/build-audit-skip.js +112 -0
  135. package/dist/src/core/build-audit.d.ts +15 -2
  136. package/dist/src/core/build-audit.js +154 -12
  137. package/dist/src/core/build-baseline.d.ts +45 -28
  138. package/dist/src/core/build-baseline.js +6 -29
  139. package/dist/src/core/build-jar-escalation.d.ts +64 -0
  140. package/dist/src/core/build-jar-escalation.js +128 -0
  141. package/dist/src/core/build-overwrite-guard.js +1 -1
  142. package/dist/src/core/build-prepare.d.ts +5 -2
  143. package/dist/src/core/build-prepare.js +34 -6
  144. package/dist/src/core/config-paths.d.ts +2 -6
  145. package/dist/src/core/config-paths.js +4 -2
  146. package/dist/src/core/config-state.d.ts +0 -6
  147. package/dist/src/core/config-state.js +1 -19
  148. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  149. package/dist/src/core/config-validate-build-audit.js +80 -0
  150. package/dist/src/core/config-validate-file-size.d.ts +12 -0
  151. package/dist/src/core/config-validate-file-size.js +65 -0
  152. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  153. package/dist/src/core/config-validate-patch-policy.js +3 -3
  154. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  155. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  156. package/dist/src/core/config-validate.js +20 -3
  157. package/dist/src/core/config.d.ts +2 -2
  158. package/dist/src/core/config.js +11 -15
  159. package/dist/src/core/coverage-extend.js +4 -4
  160. package/dist/src/core/discard-baseline.js +7 -7
  161. package/dist/src/core/engine-precondition.d.ts +1 -9
  162. package/dist/src/core/engine-precondition.js +3 -8
  163. package/dist/src/core/engine-session-lock.js +13 -17
  164. package/dist/src/core/export-moved-code-shape.js +2 -2
  165. package/dist/src/core/file-lock-owner.d.ts +103 -0
  166. package/dist/src/core/file-lock-owner.js +259 -0
  167. package/dist/src/core/file-lock.d.ts +2 -7
  168. package/dist/src/core/file-lock.js +40 -91
  169. package/dist/src/core/firefox-cache.d.ts +13 -1
  170. package/dist/src/core/firefox-cache.js +59 -34
  171. package/dist/src/core/firefox-download.d.ts +15 -0
  172. package/dist/src/core/firefox-download.js +37 -21
  173. package/dist/src/core/firefox-extract.d.ts +6 -5
  174. package/dist/src/core/firefox-extract.js +36 -18
  175. package/dist/src/core/firefox.d.ts +30 -32
  176. package/dist/src/core/firefox.js +5 -36
  177. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  178. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  179. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  180. package/dist/src/core/furnace-apply-helpers.js +8 -19
  181. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  182. package/dist/src/core/furnace-apply.d.ts +1 -1
  183. package/dist/src/core/furnace-apply.js +30 -10
  184. package/dist/src/core/furnace-config.d.ts +1 -1
  185. package/dist/src/core/furnace-config.js +23 -36
  186. package/dist/src/core/furnace-constants.js +3 -2
  187. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  188. package/dist/src/core/furnace-dir-entry.js +21 -0
  189. package/dist/src/core/furnace-operation.d.ts +2 -0
  190. package/dist/src/core/furnace-operation.js +3 -1
  191. package/dist/src/core/furnace-refresh.d.ts +18 -6
  192. package/dist/src/core/furnace-refresh.js +3 -6
  193. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  194. package/dist/src/core/furnace-registration-ast.js +0 -3
  195. package/dist/src/core/furnace-registration.d.ts +2 -1
  196. package/dist/src/core/furnace-registration.js +22 -29
  197. package/dist/src/core/furnace-scanner.js +3 -6
  198. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  199. package/dist/src/core/furnace-validate-registration.js +5 -7
  200. package/dist/src/core/git-base.d.ts +3 -1
  201. package/dist/src/core/git-base.js +25 -3
  202. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  203. package/dist/src/core/git-diff-new-file.js +119 -0
  204. package/dist/src/core/git-diff.d.ts +9 -9
  205. package/dist/src/core/git-diff.js +88 -157
  206. package/dist/src/core/git-file-ops.d.ts +0 -7
  207. package/dist/src/core/git-file-ops.js +4 -28
  208. package/dist/src/core/git-readonly-index.d.ts +0 -2
  209. package/dist/src/core/git-readonly-index.js +0 -4
  210. package/dist/src/core/git-status.d.ts +7 -0
  211. package/dist/src/core/git-status.js +21 -9
  212. package/dist/src/core/git.d.ts +0 -6
  213. package/dist/src/core/git.js +14 -43
  214. package/dist/src/core/harness-orphans.d.ts +73 -0
  215. package/dist/src/core/harness-orphans.js +187 -0
  216. package/dist/src/core/history-log.d.ts +19 -0
  217. package/dist/src/core/history-log.js +29 -0
  218. package/dist/src/core/mach-error-hints.d.ts +0 -5
  219. package/dist/src/core/mach-error-hints.js +1 -1
  220. package/dist/src/core/mach-known-noise-filter.d.ts +25 -3
  221. package/dist/src/core/mach-known-noise-filter.js +31 -10
  222. package/dist/src/core/mach-mozconfig.js +2 -1
  223. package/dist/src/core/mach-python.d.ts +2 -0
  224. package/dist/src/core/mach-python.js +12 -7
  225. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  226. package/dist/src/core/mach-resource-shim.js +112 -9
  227. package/dist/src/core/mach.d.ts +46 -53
  228. package/dist/src/core/mach.js +31 -83
  229. package/dist/src/core/marionette-port.d.ts +0 -17
  230. package/dist/src/core/marionette-port.js +12 -58
  231. package/dist/src/core/marionette-preflight.js +9 -27
  232. package/dist/src/core/mochitest-server-port.js +3 -3
  233. package/dist/src/core/moz-manifest-rules.js +37 -33
  234. package/dist/src/core/patch-apply-fuzz.js +4 -4
  235. package/dist/src/core/patch-apply.d.ts +4 -5
  236. package/dist/src/core/patch-apply.js +55 -32
  237. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  238. package/dist/src/core/patch-export-coverage.js +1 -30
  239. package/dist/src/core/patch-export-update.d.ts +18 -1
  240. package/dist/src/core/patch-export-update.js +4 -3
  241. package/dist/src/core/patch-export.d.ts +11 -21
  242. package/dist/src/core/patch-export.js +44 -66
  243. package/dist/src/core/patch-files.d.ts +0 -4
  244. package/dist/src/core/patch-files.js +0 -11
  245. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  246. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  247. package/dist/src/core/patch-lint-cache.js +3 -6
  248. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  249. package/dist/src/core/patch-lint-checkjs.js +43 -25
  250. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  251. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  252. package/dist/src/core/patch-lint-cross.js +49 -92
  253. package/dist/src/core/patch-lint-css.js +10 -6
  254. package/dist/src/core/patch-lint-file-size.d.ts +52 -0
  255. package/dist/src/core/patch-lint-file-size.js +53 -0
  256. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  257. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  258. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  259. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  260. package/dist/src/core/patch-lint-prettier.d.ts +36 -0
  261. package/dist/src/core/patch-lint-prettier.js +126 -0
  262. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  263. package/dist/src/core/patch-lint-queue-types.js +12 -0
  264. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  265. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  266. package/dist/src/core/patch-lint-unmanaged-companion.d.ts +106 -0
  267. package/dist/src/core/patch-lint-unmanaged-companion.js +214 -0
  268. package/dist/src/core/patch-lint.d.ts +14 -9
  269. package/dist/src/core/patch-lint.js +27 -27
  270. package/dist/src/core/patch-lock.js +3 -3
  271. package/dist/src/core/patch-manifest-io.js +8 -7
  272. package/dist/src/core/patch-parse.d.ts +19 -8
  273. package/dist/src/core/patch-parse.js +25 -0
  274. package/dist/src/core/patch-policy.js +15 -27
  275. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  276. package/dist/src/core/patch-source-metadata.js +0 -4
  277. package/dist/src/core/patch-transform.d.ts +21 -8
  278. package/dist/src/core/patch-transform.js +38 -10
  279. package/dist/src/core/process-cpu-time.js +1 -1
  280. package/dist/src/core/rebase-session.d.ts +2 -8
  281. package/dist/src/core/rebase-session.js +5 -17
  282. package/dist/src/core/register-browser-content.js +3 -2
  283. package/dist/src/core/register-browser-test.js +2 -1
  284. package/dist/src/core/register-module.js +2 -1
  285. package/dist/src/core/register-scaffold.js +5 -4
  286. package/dist/src/core/register-shared-css.js +2 -1
  287. package/dist/src/core/run-log-redact.d.ts +38 -0
  288. package/dist/src/core/run-log-redact.js +69 -0
  289. package/dist/src/core/run-log.d.ts +13 -2
  290. package/dist/src/core/run-log.js +67 -18
  291. package/dist/src/core/signal-critical.js +2 -4
  292. package/dist/src/core/smoke-patterns.d.ts +0 -1
  293. package/dist/src/core/smoke-patterns.js +18 -11
  294. package/dist/src/core/state-file.d.ts +12 -1
  295. package/dist/src/core/state-file.js +14 -1
  296. package/dist/src/core/status-classify.js +2 -1
  297. package/dist/src/core/test-changed-prefs.js +26 -11
  298. package/dist/src/core/test-harness-crash.js +61 -48
  299. package/dist/src/core/test-harness-verdict-notes.d.ts +55 -0
  300. package/dist/src/core/test-harness-verdict-notes.js +48 -0
  301. package/dist/src/core/test-stale-check.d.ts +23 -8
  302. package/dist/src/core/test-stale-check.js +77 -4
  303. package/dist/src/core/test-stall-triage.js +37 -19
  304. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  305. package/dist/src/core/test-xpcshell-retry.js +14 -11
  306. package/dist/src/core/token-category.d.ts +33 -0
  307. package/dist/src/core/token-category.js +56 -48
  308. package/dist/src/core/token-dark-mode.js +2 -14
  309. package/dist/src/core/token-inventory.d.ts +56 -0
  310. package/dist/src/core/token-inventory.js +109 -0
  311. package/dist/src/core/token-manager.d.ts +1 -1
  312. package/dist/src/core/token-manager.js +1 -1
  313. package/dist/src/core/token-variant.js +21 -22
  314. package/dist/src/core/toolchain-preflight.js +1 -1
  315. package/dist/src/core/tree-cow.d.ts +0 -2
  316. package/dist/src/core/tree-cow.js +2 -2
  317. package/dist/src/core/tree-store.js +4 -5
  318. package/dist/src/core/typecheck.js +2 -2
  319. package/dist/src/core/wire-dom-fragment.js +6 -17
  320. package/dist/src/core/wire-utils.js +5 -17
  321. package/dist/src/core/xpcshell-appdir.js +2 -4
  322. package/dist/src/errors/base.d.ts +53 -1
  323. package/dist/src/errors/base.js +79 -11
  324. package/dist/src/errors/build.d.ts +28 -0
  325. package/dist/src/errors/build.js +51 -10
  326. package/dist/src/errors/config.d.ts +0 -14
  327. package/dist/src/errors/config.js +13 -34
  328. package/dist/src/errors/download.d.ts +13 -0
  329. package/dist/src/errors/download.js +53 -24
  330. package/dist/src/errors/furnace.js +8 -9
  331. package/dist/src/errors/git.d.ts +8 -8
  332. package/dist/src/errors/git.js +20 -30
  333. package/dist/src/errors/patch.js +8 -10
  334. package/dist/src/errors/rebase.js +7 -7
  335. package/dist/src/types/commands/options.d.ts +96 -123
  336. package/dist/src/types/commands/patches.d.ts +17 -6
  337. package/dist/src/types/config.d.ts +75 -2
  338. package/dist/src/types/index.d.ts +0 -1
  339. package/dist/src/utils/assert.d.ts +3 -15
  340. package/dist/src/utils/assert.js +1 -18
  341. package/dist/src/utils/build-info.d.ts +1 -1
  342. package/dist/src/utils/child-env.d.ts +2 -1
  343. package/dist/src/utils/elapsed.js +10 -1
  344. package/dist/src/utils/fs.d.ts +4 -2
  345. package/dist/src/utils/fs.js +11 -14
  346. package/dist/src/utils/hash.d.ts +12 -0
  347. package/dist/src/utils/hash.js +16 -0
  348. package/dist/src/utils/line-dispatch.d.ts +15 -0
  349. package/dist/src/utils/line-dispatch.js +48 -0
  350. package/dist/src/utils/options.d.ts +1 -7
  351. package/dist/src/utils/options.js +6 -7
  352. package/dist/src/utils/package-root.js +3 -12
  353. package/dist/src/utils/platform.d.ts +0 -4
  354. package/dist/src/utils/process-group.d.ts +2 -1
  355. package/dist/src/utils/process-group.js +20 -19
  356. package/dist/src/utils/process.d.ts +40 -26
  357. package/dist/src/utils/process.js +115 -166
  358. package/dist/src/utils/ps-duration.js +3 -3
  359. package/dist/src/utils/sleep.d.ts +26 -0
  360. package/dist/src/utils/sleep.js +31 -0
  361. package/dist/src/utils/validation.d.ts +2 -38
  362. package/dist/src/utils/validation.js +7 -16
  363. package/docs/configuration.md +136 -1
  364. package/docs/exit-codes.md +34 -20
  365. package/docs/lifecycle-invariants.md +18 -12
  366. package/docs/machine-output.md +31 -8
  367. package/docs/patch-workflow.md +2 -1
  368. package/docs/run-logs.md +44 -4
  369. package/docs/testing.md +70 -11
  370. package/package.json +8 -7
  371. package/dist/src/commands/rebase.d.ts +0 -4
  372. package/dist/src/commands/rebase.js +0 -5
  373. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  374. package/dist/src/core/patch-artifact-normalize.js +0 -24
  375. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  376. package/dist/src/core/patch-lint-reexports.js +0 -8
  377. package/dist/src/core/wire-targets.d.ts +0 -7
  378. package/dist/src/core/wire-targets.js +0 -8
@@ -19,7 +19,9 @@ import { discoverPatches } from './patch-files.js';
19
19
  import { lintPatchQueueBinaryBodies } from './patch-lint-binary.js';
20
20
  import { collectNewFileCreatorsByPath } from './patch-lint-creators.js';
21
21
  import { detectNewFilesInDiff, extractAddedLinesPerFile } from './patch-lint-diff.js';
22
+ import { lintPatchQueueForwardRegistrations } from './patch-lint-forward-registration.js';
22
23
  import { lintPatchQueueModuleRegistrations } from './patch-lint-module-registration.js';
24
+ import { describeRegistrationFailure, findStagedRegistrationFailure, isLaterOwner, quoteRegistrationLine, } from './patch-lint-staged-registration.js';
23
25
  import { loadPatchesManifest } from './patch-manifest-io.js';
24
26
  import { categoryRangeForOrder, categoryRangeLabel } from './patch-policy.js';
25
27
  import { extractNewFileContent } from './patch-transform.js';
@@ -77,6 +79,9 @@ export async function buildPatchQueueContext(patchesDir, config) {
77
79
  metadata: metadataByFilename.get(patch.filename) ?? null,
78
80
  diff,
79
81
  newFiles,
82
+ // Straight from the detector, before the text-extraction filter above
83
+ // drops the bodies it cannot decode.
84
+ createdFiles: newFilePaths,
80
85
  modifiedFileAdditions,
81
86
  });
82
87
  }
@@ -125,24 +130,6 @@ const FORWARD_IMPORTABLE_EXTENSIONS = ['.mjs', '.sys.mjs', '.js', '.jsm'];
125
130
  export function isForwardImportableFile(path) {
126
131
  return FORWARD_IMPORTABLE_EXTENSIONS.some((ext) => path.endsWith(ext));
127
132
  }
128
- /**
129
- * Regex-level extractor for module specifiers in ES module / subscript files.
130
- *
131
- * Intentionally conservative. Catches:
132
- * - `import ... from "specifier"` (ES module static imports)
133
- * - `import "specifier"` (side-effect imports — the `from`
134
- * clause is optional in the regex)
135
- * - `import("specifier")` (dynamic imports)
136
- * - ChromeUtils.importESModule("specifier")
137
- * - ChromeUtils.defineESModuleGetters(obj, { Name: "specifier", ... })
138
- *
139
- * Returns the raw specifier strings — callers should take the leaf basename
140
- * to match against the newFileIndex, because we do not resolve `resource://`
141
- * URLs to engine file paths.
142
- */
143
- export function extractImportSpecifiers(source) {
144
- return extractImportSpecifiersWithLines(source).map((item) => item.specifier);
145
- }
146
133
  function buildLineOffsets(source) {
147
134
  const offsets = [0];
148
135
  for (let i = 0; i < source.length; i++) {
@@ -236,7 +223,7 @@ function collectGetterSpecifiers(stripped, results, offsetToLine) {
236
223
  }
237
224
  /**
238
225
  * Returns import specifiers plus 0-indexed line numbers, preserving the
239
- * same matching behavior as {@link extractImportSpecifiers}.
226
+ * same matching behavior as {@link extractImportSpecifiersWithLines}.
240
227
  */
241
228
  export function extractImportSpecifiersWithLines(source) {
242
229
  // stripJsComments replaces comment bodies with space runs of equal
@@ -290,8 +277,8 @@ function extractBareGetterSpecifiers(source) {
290
277
  const stripped = stripJsComments(source);
291
278
  const results = [];
292
279
  const lines = stripped.split('\n');
293
- for (let i = 0; i < lines.length; i++) {
294
- const match = BARE_GETTER_PROPERTY_LINE.exec(lines[i] ?? '');
280
+ for (const [i, line] of lines.entries()) {
281
+ const match = BARE_GETTER_PROPERTY_LINE.exec(line);
295
282
  if (match?.[1])
296
283
  results.push({ specifier: match[1], line: i });
297
284
  }
@@ -323,9 +310,8 @@ export const FORWARD_IMPORT_IGNORE_MARKER = 'fireforge-ignore: forward-import';
323
310
  export function findForwardImportIgnoreLines(source) {
324
311
  const lines = source.split('\n');
325
312
  const ignored = new Set();
326
- for (let i = 0; i < lines.length; i++) {
327
- const line = lines[i];
328
- if (line && line.includes(FORWARD_IMPORT_IGNORE_MARKER)) {
313
+ for (const [i, line] of lines.entries()) {
314
+ if (line.includes(FORWARD_IMPORT_IGNORE_MARKER)) {
329
315
  ignored.add(i);
330
316
  ignored.add(i + 1);
331
317
  }
@@ -348,29 +334,44 @@ function findMatchingStagedDependency(entry, sitePath, specifier, laterOwners) {
348
334
  laterOwners.some((owner) => owner.fullPath === dependency.creates &&
349
335
  (dependency.owner === undefined || dependency.owner === owner.filename)));
350
336
  }
351
- /**
352
- * Later-in-apply-order predicate shared by the forward-import scan and the
353
- * registration validation: strictly higher ordinal, or same ordinal with a
354
- * lexicographically later filename (the apply loop's tiebreak).
355
- */
356
- function isLaterOwner(owner, entry) {
357
- return (owner.order > entry.order || (owner.order === entry.order && owner.filename > entry.filename));
358
- }
359
- /** Builds the basename → later-creators index used by the forward-import scan. */
360
- function buildNewFileIndex(ctx) {
361
- const newFileIndex = new Map();
337
+ /** Builds a basename → creators index from one path set per entry. */
338
+ function buildCreatorIndex(ctx, pathsOf) {
339
+ const index = new Map();
362
340
  for (const entry of ctx.entries) {
363
- for (const fullPath of entry.newFiles.keys()) {
341
+ for (const fullPath of pathsOf(entry)) {
364
342
  const leaf = basename(fullPath);
365
- let owners = newFileIndex.get(leaf);
343
+ let owners = index.get(leaf);
366
344
  if (!owners) {
367
345
  owners = [];
368
- newFileIndex.set(leaf, owners);
346
+ index.set(leaf, owners);
369
347
  }
370
348
  owners.push({ filename: entry.filename, order: entry.order, fullPath });
371
349
  }
372
350
  }
373
- return newFileIndex;
351
+ return index;
352
+ }
353
+ /**
354
+ * Basename → later-creators index for the forward-IMPORT scan.
355
+ *
356
+ * Keyed on `newFiles` deliberately: this index answers "which patch creates
357
+ * the module this specifier resolves to", and a binary is not a module.
358
+ */
359
+ function buildNewFileIndex(ctx) {
360
+ return buildCreatorIndex(ctx, (entry) => entry.newFiles.keys());
361
+ }
362
+ /**
363
+ * Basename → creators index for the staged-REGISTRATION rules.
364
+ *
365
+ * Keyed on `createdFiles`, which includes binary creations. A registration
366
+ * (`support-files`, a jar.mn line, an actor entry) points at a file that
367
+ * must EXIST after apply; whether its body was text or a `GIT binary patch`
368
+ * has nothing to do with it. Resolving this from `newFiles` made a declared
369
+ * dependency on a binary fixture report `creates-unresolved`, so the
370
+ * declaration read as stale on every lint run and removing it changed
371
+ * nothing either — the failure was symmetric and therefore invisible.
372
+ */
373
+ function buildCreatedFileIndex(ctx) {
374
+ return buildCreatorIndex(ctx, (entry) => entry.createdFiles);
374
375
  }
375
376
  /**
376
377
  * Visits every forward-import site (an import whose leaf is created by a
@@ -389,9 +390,9 @@ function eachForwardImportSite(ctx, newFileIndex, visit) {
389
390
  // Dedupe by (specifier, line): a patch that adds the whole getter
390
391
  // call carries both the opener (found by the balanced walk) and the
391
392
  // property line (found here) — report each site once.
392
- const seen = new Set(sites.map((site) => `${site.specifier}|${String(site.line)}`));
393
+ const seen = new Set(sites.map((site) => `${site.specifier}|${site.line}`));
393
394
  for (const extra of extractBareGetterSpecifiers(content)) {
394
- if (!seen.has(`${extra.specifier}|${String(extra.line)}`))
395
+ if (!seen.has(`${extra.specifier}|${extra.line}`))
395
396
  sites.push(extra);
396
397
  }
397
398
  }
@@ -453,6 +454,7 @@ export function collectForwardImportEdges(ctx) {
453
454
  */
454
455
  export function lintPatchQueueForwardImports(ctx) {
455
456
  const newFileIndex = buildNewFileIndex(ctx);
457
+ const createdFileIndex = buildCreatedFileIndex(ctx);
456
458
  const issues = [];
457
459
  const usedStagedDeclarations = new Set();
458
460
  eachForwardImportSite(ctx, newFileIndex, (entry, sitePath, specifier, cleaned, laterOwners) => {
@@ -531,7 +533,7 @@ export function lintPatchQueueForwardImports(ctx) {
531
533
  // (with `owner`, when set, naming that patch) — mirroring
532
534
  // findMatchingStagedDependency for the import kind.
533
535
  for (const registration of entry.metadata?.stagedDependencies?.registrations ?? []) {
534
- const failure = findStagedRegistrationFailure(entry, registration, newFileIndex);
536
+ const failure = findStagedRegistrationFailure(entry, registration, createdFileIndex);
535
537
  if (failure === undefined)
536
538
  continue;
537
539
  issues.push({
@@ -541,62 +543,16 @@ export function lintPatchQueueForwardImports(ctx) {
541
543
  fingerprint: `staged-dependency-unused|${entry.filename}|${registration.file}|reg:${registration.line}|${registration.creates}|${registration.owner ?? ''}`,
542
544
  message: `${entry.filename} declares a staged registration in ${registration.file} ` +
543
545
  `("${registration.line}") for ${registration.creates}, but ${describeRegistrationFailure(failure)} ` +
544
- 'Remove the stale declaration with "fireforge patch staged-dependency --remove --kind registration" or update it to match the patch and queue.',
546
+ 'Remove the stale declaration with: ' +
547
+ `fireforge patch staged-dependency ${entry.filename} --remove --kind registration ` +
548
+ `--file ${registration.file} --line "${quoteRegistrationLine(registration.line)}" ` +
549
+ `--creates ${registration.creates}; or update it to match the patch and queue.`,
545
550
  severity: 'warning',
546
551
  });
547
552
  }
548
553
  }
549
554
  return issues;
550
555
  }
551
- /**
552
- * Validates one registration-kind declaration against the patch content
553
- * and the queue, mirroring {@link findMatchingStagedDependency}: the
554
- * declared line must be added by this patch, `creates` must exactly match
555
- * a file a LATER-ordered patch creates (an earlier-only creator means the
556
- * dependency is already satisfied and the declaration is stale), and
557
- * `owner`, when set, must name one of those creating patches. Returns
558
- * undefined when valid.
559
- */
560
- function findStagedRegistrationFailure(entry, registration, newFileIndex) {
561
- if (!isRegistrationLinePresent(entry, registration))
562
- return { kind: 'line-absent' };
563
- const creators = (newFileIndex.get(basename(registration.creates)) ?? []).filter((owner) => owner.fullPath === registration.creates && isLaterOwner(owner, entry));
564
- if (creators.length === 0)
565
- return { kind: 'creates-unresolved' };
566
- if (registration.owner !== undefined &&
567
- !creators.some((owner) => owner.filename === registration.owner)) {
568
- return { kind: 'owner-mismatch', creators };
569
- }
570
- return undefined;
571
- }
572
- /** Message tail for each {@link StagedRegistrationFailure} shape. */
573
- function describeRegistrationFailure(failure) {
574
- switch (failure.kind) {
575
- case 'line-absent':
576
- return 'the patch does not add that line.';
577
- case 'creates-unresolved':
578
- return ('no later-ordered patch creates that file — the declaration is stale, ' +
579
- 'or the queue order no longer stages it.');
580
- case 'owner-mismatch':
581
- return `that file is created by ${failure.creators.map((o) => o.filename).join(', ')}, not the declared owner.`;
582
- }
583
- }
584
- /**
585
- * True when the declared registration/packaging line appears (trimmed)
586
- * among the lines the patch introduces in the declaring file — either as a
587
- * newly-created file's content or as added lines to an existing file
588
- * (where jar.mn / customElements.js / actor-registration edits land).
589
- */
590
- function isRegistrationLinePresent(entry, registration) {
591
- const declared = registration.line.trim();
592
- if (declared.length === 0)
593
- return false;
594
- const introduced = [
595
- entry.newFiles.get(registration.file),
596
- entry.modifiedFileAdditions.get(registration.file),
597
- ];
598
- return introduced.some((content) => content !== undefined && content.split('\n').some((line) => line.trim() === declared));
599
- }
600
556
  /**
601
557
  * Cross-patch lint orchestrator. Runs every cross-patch rule against the
602
558
  * provided context and returns combined issues.
@@ -614,6 +570,7 @@ export function lintPatchQueue(ctx) {
614
570
  ...lintPatchQueueDuplicateCreations(ctx),
615
571
  ...lintPatchQueueForwardImports(ctx),
616
572
  ...lintPatchQueueModuleRegistrations(ctx),
573
+ ...lintPatchQueueForwardRegistrations(ctx),
617
574
  ...lintPatchQueueBinaryBodies(ctx),
618
575
  ];
619
576
  }
@@ -73,11 +73,8 @@ function buildAddedLinesScanSource(rawCss, addedNumbers) {
73
73
  .map((n) => maskedLines[n - 1] ?? '')
74
74
  .join('\n');
75
75
  }
76
- /**
77
- * Raw-color check for one patched CSS file, scoped to introduced lines
78
- * when diff context is available. Pushes onto `issues`.
79
- */
80
- function checkRawColorValues(file, rawCss, addedLinesByFile, addedLineNumbersByFile, config, issues) {
76
+ function checkRawColorValues(input) {
77
+ const { file, rawCss, addedLinesByFile, addedLineNumbersByFile, config, issues } = input;
81
78
  // Check only introduced raw color values when diff context is available.
82
79
  // Skip files on the raw-color allowlist (exact path or basename match) and
83
80
  // auto-exempt files under `browser/branding/` — those are the fork's
@@ -207,7 +204,14 @@ export async function lintPatchedCss(repoDir, affectedFiles, diffContent, config
207
204
  const rawCss = await readText(filePath);
208
205
  // Strip block comments before scanning
209
206
  const cssContent = rawCss.replace(/\/\*[\s\S]*?\*\//g, '');
210
- checkRawColorValues(file, rawCss, addedLinesByFile, addedLineNumbersByFile, config, issues);
207
+ checkRawColorValues({
208
+ file,
209
+ rawCss,
210
+ addedLinesByFile,
211
+ addedLineNumbersByFile,
212
+ config,
213
+ issues,
214
+ });
211
215
  checkTokenPrefixViolations(file, cssContent, addedLinesByFile, tokenContext, issues);
212
216
  }
213
217
  return issues;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The `file-too-large` thresholds and their rendered message.
3
+ *
4
+ * Its own module for two reasons: the config validator needs the DEFAULTS
5
+ * to validate an override triple against the merged result, and importing
6
+ * `patch-lint.ts` from `config-validate.ts` for a constant would draw an
7
+ * import edge (and a cycle) between the rule bodies and config parsing.
8
+ *
9
+ * On the vocabulary: the bands were `notice` / `warning` / `error` with the
10
+ * middle one described as a "soft limit". That description is false under
11
+ * the posture FireForge itself recommends for a release gate,
12
+ * `--max-warnings 0`, where a warning is a hard failure — so a file at 751
13
+ * lines failed a gate with a message calling 750 soft. The message below
14
+ * says what the band IS (a warning) and what a zero-warning gate does with
15
+ * it, and the thresholds are now tunable, so a project whose file-size
16
+ * policy differs has a dial instead of an argument.
17
+ */
18
+ import type { PatchLintFileSizeThresholds } from '../types/config.js';
19
+ /** A fully resolved threshold triple. */
20
+ export interface ResolvedFileSizeTier {
21
+ notice: number;
22
+ warning: number;
23
+ error: number;
24
+ }
25
+ /** Built-in defaults, per file class. */
26
+ export declare const DEFAULT_FILE_SIZE_THRESHOLDS: {
27
+ general: ResolvedFileSizeTier;
28
+ test: ResolvedFileSizeTier;
29
+ };
30
+ /**
31
+ * Merges a configured override over the defaults for one file class.
32
+ *
33
+ * @param overrides - The `patchLint.fileSizeThresholds` block, if any
34
+ * @param isTest - Whether the file counts as a test file
35
+ * @returns The effective triple for this file
36
+ */
37
+ export declare function resolveFileSizeThresholds(overrides: PatchLintFileSizeThresholds | undefined, isTest: boolean): ResolvedFileSizeTier;
38
+ /**
39
+ * Renders the `file-too-large` message for a given band.
40
+ *
41
+ * The `warning` and `notice` bands name the gate consequence explicitly:
42
+ * the number is advisory to patch-lint and blocking to a `--max-warnings 0`
43
+ * gate, and an operator deciding whether to decompose a file needs to know
44
+ * which of those they are looking at.
45
+ */
46
+ export declare function formatFileTooLargeMessage(args: {
47
+ label: string;
48
+ lineCount: number;
49
+ thresholds: ResolvedFileSizeTier;
50
+ band: 'notice' | 'warning' | 'error';
51
+ verb: string;
52
+ }): string;
@@ -0,0 +1,53 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * The `file-too-large` thresholds and their rendered message.
4
+ *
5
+ * Its own module for two reasons: the config validator needs the DEFAULTS
6
+ * to validate an override triple against the merged result, and importing
7
+ * `patch-lint.ts` from `config-validate.ts` for a constant would draw an
8
+ * import edge (and a cycle) between the rule bodies and config parsing.
9
+ *
10
+ * On the vocabulary: the bands were `notice` / `warning` / `error` with the
11
+ * middle one described as a "soft limit". That description is false under
12
+ * the posture FireForge itself recommends for a release gate,
13
+ * `--max-warnings 0`, where a warning is a hard failure — so a file at 751
14
+ * lines failed a gate with a message calling 750 soft. The message below
15
+ * says what the band IS (a warning) and what a zero-warning gate does with
16
+ * it, and the thresholds are now tunable, so a project whose file-size
17
+ * policy differs has a dial instead of an argument.
18
+ */
19
+ /** Built-in defaults, per file class. */
20
+ export const DEFAULT_FILE_SIZE_THRESHOLDS = {
21
+ general: { notice: 500, warning: 750, error: 900 },
22
+ test: { notice: 1200, warning: 1400, error: 1600 },
23
+ };
24
+ /**
25
+ * Merges a configured override over the defaults for one file class.
26
+ *
27
+ * @param overrides - The `patchLint.fileSizeThresholds` block, if any
28
+ * @param isTest - Whether the file counts as a test file
29
+ * @returns The effective triple for this file
30
+ */
31
+ export function resolveFileSizeThresholds(overrides, isTest) {
32
+ const key = isTest ? 'test' : 'general';
33
+ const configured = overrides?.[key] ?? {};
34
+ return { ...DEFAULT_FILE_SIZE_THRESHOLDS[key], ...configured };
35
+ }
36
+ /**
37
+ * Renders the `file-too-large` message for a given band.
38
+ *
39
+ * The `warning` and `notice` bands name the gate consequence explicitly:
40
+ * the number is advisory to patch-lint and blocking to a `--max-warnings 0`
41
+ * gate, and an operator deciding whether to decompose a file needs to know
42
+ * which of those they are looking at.
43
+ */
44
+ export function formatFileTooLargeMessage(args) {
45
+ const { label, lineCount, thresholds, band, verb } = args;
46
+ if (band === 'error') {
47
+ return `${label} has ${lineCount} lines (error threshold: ${thresholds.error}). Consider ${verb}.`;
48
+ }
49
+ return (`${label} has ${lineCount} lines (warning threshold: ${thresholds.warning}, ` +
50
+ `error threshold: ${thresholds.error}). Consider ${verb}. ` +
51
+ `Reported as a ${band}, which a gate running --max-warnings 0 treats as a failure; ` +
52
+ `tune "patchLint.fileSizeThresholds" if these limits are wrong for this project.`);
53
+ }
@@ -0,0 +1,58 @@
1
+ import type { PatchLintIssue } from '../types/commands/index.js';
2
+ import type { PatchQueueForwardRegistrationEntry, PatchQueueView } from './patch-lint-queue-types.js';
3
+ /**
4
+ * One `support-files` entry, with the line the patch actually adds it on.
5
+ *
6
+ * The entry alone is not enough for the discharge command the rule prints:
7
+ * `--line` is compared, whitespace-trimmed, against the lines the patch
8
+ * ADDS, and a synthesised `support-files = ["<entry>"]` only ever matches a
9
+ * manifest whose array is single-line and single-entry. Carrying the source
10
+ * line keeps the remedy and the validation talking about the same text.
11
+ */
12
+ export interface SupportFileEntry {
13
+ /** The entry as written inside the manifest (e.g. `fixtures/*.sqlite`). */
14
+ entry: string;
15
+ /** The first added line, whitespace-trimmed, that spells the entry out. */
16
+ line: string;
17
+ /**
18
+ * How many distinct added lines spell it out. Above one, the printed
19
+ * command quotes the first and says so: one declaration covers the file
20
+ * either way, because the declared check keys on file + creates.
21
+ */
22
+ occurrences: number;
23
+ }
24
+ /**
25
+ * Extracts the `support-files` entries declared in `content`, each paired
26
+ * with the manifest line it is written on.
27
+ *
28
+ * Handles the TOML array on one line and spread over several, and the
29
+ * whitespace-separated `.ini` spelling. Anything else — a computed value, a
30
+ * shape this does not recognise — yields nothing, because a rule that
31
+ * guesses at a manifest it cannot parse refuses correct queues.
32
+ *
33
+ * Quoted tokens are matched per physical line rather than over the whole
34
+ * accumulated array body, so a multi-line array attributes each element to
35
+ * its own line. Entries are deduplicated: a manifest that lists the same
36
+ * file under two per-test sections needs one declaration, not two.
37
+ */
38
+ export declare function extractSupportFileEntries(content: string): SupportFileEntry[];
39
+ /**
40
+ * Resolves one `support-files` entry, relative to its manifest's directory,
41
+ * into a matcher over engine-relative paths.
42
+ *
43
+ * Only a `*` inside a single path segment is expanded — the shape the
44
+ * downstream report uses (`fixtures/*.sqlite`). `**`, `!` exclusions and
45
+ * absolute `/`-rooted entries return undefined: they either cross directory
46
+ * boundaries or subtract, and a matcher that guessed at them would attribute
47
+ * a creation to the wrong manifest.
48
+ */
49
+ export declare function buildSupportFileMatcher(manifestPath: string, entry: string): ((candidate: string) => boolean) | undefined;
50
+ /**
51
+ * Flags every `support-files` registration a patch introduces that names a
52
+ * file only a later-ordered patch creates, unless the staged dependency is
53
+ * declared.
54
+ *
55
+ * @param ctx - Projected queue in application order
56
+ * @returns One issue per undeclared forward registration
57
+ */
58
+ export declare function lintPatchQueueForwardRegistrations(ctx: PatchQueueView<PatchQueueForwardRegistrationEntry>): PatchLintIssue[];
@@ -0,0 +1,210 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Cross-patch lint rule: a test manifest that registers a file a
4
+ * LATER-ordered patch creates, without declaring the staged dependency.
5
+ *
6
+ * The import kind has had this arm since forward-import shipped; the
7
+ * registration kind never did. `--kind registration` metadata was only ever
8
+ * checked for STALENESS once declared, so a patch whose `xpcshell.toml`
9
+ * carried `support-files = ["fixtures/*.sqlite"]` for a fixture the next
10
+ * patch creates passed lint in both arms — declaring the dependency and
11
+ * removing the declaration produced identical silence, which is what made
12
+ * the gap invisible rather than merely absent.
13
+ *
14
+ * Scope is deliberately narrow: test-manifest `support-files` only. A
15
+ * jar.mn packaging line or an actor/customElements registration names its
16
+ * target through indirection the linter cannot resolve without the build
17
+ * system, and a rule that guesses there would refuse correct queues on a
18
+ * Firefox-sized tree. The staged-registration declaration remains the
19
+ * documented escape hatch for a deliberate stage.
20
+ */
21
+ import { basename, dirname, posix } from 'node:path';
22
+ import { normalizePathSlashes } from '../utils/paths.js';
23
+ import { escapeRegex } from '../utils/regex.js';
24
+ import { isLaterOwner, quoteRegistrationLine } from './patch-lint-staged-registration.js';
25
+ /**
26
+ * Manifest basenames whose `support-files` key registers auxiliary files
27
+ * for a test suite. Closed and documented on purpose — a pattern like
28
+ * "anything .toml" would sweep in Cargo and taskcluster manifests, whose
29
+ * `support-files`-shaped keys mean something else entirely.
30
+ */
31
+ const TEST_MANIFEST_BASENAMES = new Set([
32
+ 'xpcshell.toml',
33
+ 'xpcshell.ini',
34
+ 'mochitest.toml',
35
+ 'mochitest.ini',
36
+ 'browser.toml',
37
+ 'browser.ini',
38
+ 'chrome.toml',
39
+ 'chrome.ini',
40
+ 'a11y.toml',
41
+ 'a11y.ini',
42
+ ]);
43
+ /** True for a path whose basename is a recognised test manifest. */
44
+ function isTestManifestPath(path) {
45
+ return TEST_MANIFEST_BASENAMES.has(basename(normalizePathSlashes(path)));
46
+ }
47
+ /** Matches the start of a `support-files` assignment. */
48
+ const SUPPORT_FILES_PATTERN = /^\s*support-files\s*=\s*(.*)$/;
49
+ /**
50
+ * Extracts the `support-files` entries declared in `content`, each paired
51
+ * with the manifest line it is written on.
52
+ *
53
+ * Handles the TOML array on one line and spread over several, and the
54
+ * whitespace-separated `.ini` spelling. Anything else — a computed value, a
55
+ * shape this does not recognise — yields nothing, because a rule that
56
+ * guesses at a manifest it cannot parse refuses correct queues.
57
+ *
58
+ * Quoted tokens are matched per physical line rather than over the whole
59
+ * accumulated array body, so a multi-line array attributes each element to
60
+ * its own line. Entries are deduplicated: a manifest that lists the same
61
+ * file under two per-test sections needs one declaration, not two.
62
+ */
63
+ export function extractSupportFileEntries(content) {
64
+ const lines = content.split('\n');
65
+ const order = [];
66
+ const sourceLines = new Map();
67
+ const record = (entry, source) => {
68
+ let seen = sourceLines.get(entry);
69
+ if (seen === undefined) {
70
+ seen = [];
71
+ sourceLines.set(entry, seen);
72
+ order.push(entry);
73
+ }
74
+ const trimmed = source.trim();
75
+ if (!seen.includes(trimmed))
76
+ seen.push(trimmed);
77
+ };
78
+ for (let i = 0; i < lines.length; i++) {
79
+ const raw = lines[i] ?? '';
80
+ const match = SUPPORT_FILES_PATTERN.exec(raw);
81
+ if (!match)
82
+ continue;
83
+ const head = match[1] ?? '';
84
+ if (!head.trimStart().startsWith('[')) {
85
+ // `.ini` spelling: whitespace-separated bare paths on one line.
86
+ for (const token of head.split(/\s+/).filter((t) => t.length > 0))
87
+ record(token, raw);
88
+ continue;
89
+ }
90
+ // TOML array: walk the physical lines until the closing bracket,
91
+ // bounded so an unterminated array in a malformed manifest cannot walk
92
+ // the file.
93
+ let body = head;
94
+ let scanned = 0;
95
+ let current = raw;
96
+ for (;;) {
97
+ for (const quoted of current.matchAll(/["']([^"']+)["']/g)) {
98
+ if (quoted[1] !== undefined)
99
+ record(quoted[1], current);
100
+ }
101
+ if (body.includes(']') || i + 1 >= lines.length || scanned >= 200)
102
+ break;
103
+ i += 1;
104
+ scanned += 1;
105
+ current = lines[i] ?? '';
106
+ body += `\n${current}`;
107
+ }
108
+ }
109
+ return order.map((entry) => {
110
+ const seen = sourceLines.get(entry) ?? [];
111
+ return { entry, line: seen[0] ?? '', occurrences: seen.length };
112
+ });
113
+ }
114
+ /**
115
+ * Resolves one `support-files` entry, relative to its manifest's directory,
116
+ * into a matcher over engine-relative paths.
117
+ *
118
+ * Only a `*` inside a single path segment is expanded — the shape the
119
+ * downstream report uses (`fixtures/*.sqlite`). `**`, `!` exclusions and
120
+ * absolute `/`-rooted entries return undefined: they either cross directory
121
+ * boundaries or subtract, and a matcher that guessed at them would attribute
122
+ * a creation to the wrong manifest.
123
+ */
124
+ export function buildSupportFileMatcher(manifestPath, entry) {
125
+ if (entry.startsWith('!') || entry.includes('**') || entry.startsWith('/'))
126
+ return undefined;
127
+ const resolved = posix.normalize(posix.join(normalizePathSlashes(dirname(manifestPath)), normalizePathSlashes(entry)));
128
+ if (resolved.startsWith('..'))
129
+ return undefined;
130
+ if (!resolved.includes('*')) {
131
+ return (candidate) => normalizePathSlashes(candidate) === resolved;
132
+ }
133
+ const pattern = new RegExp(`^${resolved
134
+ .split('*')
135
+ .map((part) => escapeRegex(part))
136
+ .join('[^/]*')}$`);
137
+ return (candidate) => pattern.test(normalizePathSlashes(candidate));
138
+ }
139
+ /** True when this patch already declares the staged registration. */
140
+ function isDeclared(entry, manifestPath, createdPath) {
141
+ return (entry.metadata?.stagedDependencies?.registrations ?? []).some((registration) => normalizePathSlashes(registration.file) === normalizePathSlashes(manifestPath) &&
142
+ normalizePathSlashes(registration.creates) === normalizePathSlashes(createdPath));
143
+ }
144
+ /**
145
+ * Flags every `support-files` registration a patch introduces that names a
146
+ * file only a later-ordered patch creates, unless the staged dependency is
147
+ * declared.
148
+ *
149
+ * @param ctx - Projected queue in application order
150
+ * @returns One issue per undeclared forward registration
151
+ */
152
+ export function lintPatchQueueForwardRegistrations(ctx) {
153
+ const issues = [];
154
+ for (const entry of ctx.entries) {
155
+ // Both arms: a manifest this patch CREATES and one it MODIFIES can each
156
+ // introduce a registration line.
157
+ const manifests = [
158
+ ...entry.newFiles,
159
+ ...entry.modifiedFileAdditions,
160
+ ].filter(([path]) => isTestManifestPath(path));
161
+ for (const [manifestPath, content] of manifests) {
162
+ for (const { entry: raw, line: addedLine, occurrences } of extractSupportFileEntries(content)) {
163
+ const matches = buildSupportFileMatcher(manifestPath, raw);
164
+ if (matches === undefined)
165
+ continue;
166
+ // `--line` is the line as the patch adds it, which is what the
167
+ // declared arm compares and what `--remove` matches on. Synthesising
168
+ // `support-files = ["<entry>"]` instead only ever matched a
169
+ // single-line single-entry array, so the pasted command left the
170
+ // declaring patch red with staged-dependency-unused.
171
+ const declaredLine = quoteRegistrationLine(addedLine);
172
+ const ambiguity = occurrences > 1
173
+ ? ` (the entry is spelled out on ${occurrences} added lines here; ` +
174
+ 'the first is quoted, and one declaration covers the file either way ' +
175
+ 'because the declared check keys on --file and --creates.)'
176
+ : '';
177
+ for (const later of ctx.entries) {
178
+ // Same predicate the declared-registration arm validates against,
179
+ // tiebreak included: two spellings of "later" would let one rule
180
+ // flag a pair the other considers already satisfied.
181
+ if (!isLaterOwner(later, entry))
182
+ continue;
183
+ for (const created of later.createdFiles) {
184
+ if (!matches(created))
185
+ continue;
186
+ if (isDeclared(entry, manifestPath, created))
187
+ continue;
188
+ issues.push({
189
+ file: manifestPath,
190
+ check: 'forward-registration',
191
+ patches: [entry.filename],
192
+ fingerprint: `forward-registration|${entry.filename}|${manifestPath}|${raw}|${created}|${later.filename}`,
193
+ message: `${manifestPath} in ${entry.filename} registers "${raw}" as a support file, ` +
194
+ `but ${created} is created by the later patch ${later.filename}. ` +
195
+ 'Applying the queue up to this patch leaves the registration dangling. ' +
196
+ 'Reorder the patches so the file is created first, move the registration into ' +
197
+ 'the later patch, or declare the intentional staged dependency with: ' +
198
+ `fireforge patch staged-dependency ${entry.filename} --add --kind registration ` +
199
+ `--file ${manifestPath} --line "${declaredLine}" ` +
200
+ `--creates ${created} --owner ${later.filename}` +
201
+ ambiguity,
202
+ severity: 'error',
203
+ });
204
+ }
205
+ }
206
+ }
207
+ }
208
+ }
209
+ return issues;
210
+ }