@hominis/fireforge 0.40.0 → 0.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.md +16 -10
- package/dist/bin/fireforge.js +25 -4
- package/dist/build-info.json +9 -0
- package/dist/src/cli.js +20 -3
- package/dist/src/commands/bootstrap.js +5 -1
- package/dist/src/commands/discard.js +2 -22
- package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
- package/dist/src/commands/doctor-check-core.d.ts +20 -0
- package/dist/src/commands/doctor-check-core.js +28 -0
- package/dist/src/commands/doctor-external-toolchains.js +2 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
- package/dist/src/commands/doctor-furnace.js +13 -11
- package/dist/src/commands/doctor-orphaned-harness.js +2 -1
- package/dist/src/commands/doctor.js +2 -2
- package/dist/src/commands/export-all.js +9 -1
- package/dist/src/commands/export-flow.js +13 -3
- package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
- package/dist/src/commands/export-placement-conflicts.js +74 -0
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.d.ts +8 -1
- package/dist/src/commands/export-shared.js +16 -7
- package/dist/src/commands/export.js +2 -2
- package/dist/src/commands/furnace/apply.js +4 -3
- package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
- package/dist/src/commands/furnace/chrome-doc.js +13 -2
- package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
- package/dist/src/commands/furnace/create-browser-test.js +5 -0
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/diff.js +2 -1
- package/dist/src/commands/furnace/init.js +4 -6
- package/dist/src/commands/furnace/override.js +2 -2
- package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
- package/dist/src/commands/furnace/remove-mutations.js +171 -0
- package/dist/src/commands/furnace/remove.js +21 -128
- package/dist/src/commands/furnace/rename-browser-test.js +54 -5
- package/dist/src/commands/furnace/scan.js +27 -36
- package/dist/src/commands/furnace/status.js +30 -28
- package/dist/src/commands/furnace/validate.js +25 -18
- package/dist/src/commands/import.js +6 -20
- package/dist/src/commands/lint-per-patch.js +12 -59
- package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
- package/dist/src/commands/lint-per-run-checkjs.js +102 -0
- package/dist/src/commands/lint.js +4 -4
- package/dist/src/commands/manifest.js +1 -1
- package/dist/src/commands/package.js +9 -16
- package/dist/src/commands/patch/compact.js +5 -5
- package/dist/src/commands/patch/delete.js +5 -5
- package/dist/src/commands/patch/lint-ignore.js +3 -2
- package/dist/src/commands/patch/move-files-create.js +22 -4
- package/dist/src/commands/patch/move-files-into.js +14 -10
- package/dist/src/commands/patch/move-files.js +6 -5
- package/dist/src/commands/patch/rename.d.ts +2 -2
- package/dist/src/commands/patch/rename.js +151 -50
- package/dist/src/commands/patch/reorder.d.ts +9 -0
- package/dist/src/commands/patch/reorder.js +14 -10
- package/dist/src/commands/patch/split-plan.d.ts +13 -6
- package/dist/src/commands/patch/split-plan.js +22 -12
- package/dist/src/commands/patch/split.d.ts +1 -1
- package/dist/src/commands/patch/split.js +23 -9
- package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
- package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
- package/dist/src/commands/patch/staged-dependency.js +10 -0
- package/dist/src/commands/re-export-adjacent.js +5 -6
- package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
- package/dist/src/commands/re-export-bulk-scan.js +142 -2
- package/dist/src/commands/re-export-drift.d.ts +74 -0
- package/dist/src/commands/re-export-drift.js +234 -0
- package/dist/src/commands/re-export-files.js +5 -3
- package/dist/src/commands/re-export-lint.d.ts +97 -0
- package/dist/src/commands/re-export-lint.js +210 -0
- package/dist/src/commands/re-export-options.js +8 -0
- package/dist/src/commands/re-export-register.d.ts +18 -0
- package/dist/src/commands/re-export-register.js +61 -0
- package/dist/src/commands/re-export-scan.js +13 -7
- package/dist/src/commands/re-export-single.d.ts +20 -0
- package/dist/src/commands/re-export-single.js +261 -0
- package/dist/src/commands/re-export.d.ts +0 -4
- package/dist/src/commands/re-export.js +113 -268
- package/dist/src/commands/rebase/abort.js +13 -4
- package/dist/src/commands/rebase/continue.js +11 -4
- package/dist/src/commands/rebase/index.js +19 -29
- package/dist/src/commands/rebase/patch-loop.js +12 -0
- package/dist/src/commands/reset.js +4 -23
- package/dist/src/commands/resolve.js +2 -9
- package/dist/src/commands/run.js +11 -21
- package/dist/src/commands/source.js +3 -9
- package/dist/src/commands/status-check.d.ts +13 -0
- package/dist/src/commands/status-check.js +31 -13
- package/dist/src/commands/status-json.d.ts +31 -0
- package/dist/src/commands/status-json.js +76 -0
- package/dist/src/commands/status-output.d.ts +1 -0
- package/dist/src/commands/status-output.js +10 -1
- package/dist/src/commands/status-ownership.d.ts +52 -0
- package/dist/src/commands/status-ownership.js +57 -0
- package/dist/src/commands/status.js +184 -175
- package/dist/src/commands/test-build-phase.d.ts +19 -0
- package/dist/src/commands/test-build-phase.js +168 -0
- package/dist/src/commands/test-diagnose.js +30 -2
- package/dist/src/commands/test-modes.d.ts +9 -1
- package/dist/src/commands/test-modes.js +46 -13
- package/dist/src/commands/test-register.js +59 -27
- package/dist/src/commands/test-run.d.ts +29 -3
- package/dist/src/commands/test-run.js +41 -4
- package/dist/src/commands/test-verdict.d.ts +32 -0
- package/dist/src/commands/test-verdict.js +48 -0
- package/dist/src/commands/test.d.ts +9 -0
- package/dist/src/commands/test.js +100 -123
- package/dist/src/commands/token-coverage.js +4 -9
- package/dist/src/commands/tree.d.ts +3 -0
- package/dist/src/commands/tree.js +171 -25
- package/dist/src/commands/verify.js +2 -2
- package/dist/src/commands/watch.js +22 -21
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.d.ts +11 -0
- package/dist/src/core/ast-utils.js +11 -0
- package/dist/src/core/build-audit-platform.js +6 -1
- package/dist/src/core/build-audit-registration.js +2 -0
- package/dist/src/core/build-audit-resolve.d.ts +13 -0
- package/dist/src/core/build-audit-resolve.js +34 -13
- package/dist/src/core/build-audit.js +4 -20
- package/dist/src/core/build-baseline-types.d.ts +29 -8
- package/dist/src/core/build-baseline-types.js +2 -1
- package/dist/src/core/build-baseline.d.ts +10 -2
- package/dist/src/core/build-baseline.js +39 -8
- package/dist/src/core/build-prepare.d.ts +4 -0
- package/dist/src/core/build-prepare.js +7 -1
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +3 -2
- package/dist/src/core/config-validate.js +34 -36
- package/dist/src/core/config.js +16 -3
- package/dist/src/core/coverage-extend.d.ts +89 -0
- package/dist/src/core/coverage-extend.js +188 -0
- package/dist/src/core/destructive.js +3 -1
- package/dist/src/core/discard-baseline.js +18 -4
- package/dist/src/core/engine-precondition.d.ts +35 -0
- package/dist/src/core/engine-precondition.js +50 -0
- package/dist/src/core/engine-session-lock.d.ts +38 -1
- package/dist/src/core/engine-session-lock.js +78 -4
- package/dist/src/core/file-lock.d.ts +7 -0
- package/dist/src/core/file-lock.js +20 -30
- package/dist/src/core/firefox.js +5 -2
- package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
- package/dist/src/core/furnace-apply-helpers.js +12 -3
- package/dist/src/core/furnace-apply-output.js +9 -0
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
- package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
- package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
- package/dist/src/core/furnace-apply.d.ts +16 -4
- package/dist/src/core/furnace-apply.js +39 -43
- package/dist/src/core/furnace-config.js +2 -0
- package/dist/src/core/furnace-css-fragments.js +2 -0
- package/dist/src/core/furnace-jsconfig.js +14 -7
- package/dist/src/core/furnace-registration-ast.js +8 -8
- package/dist/src/core/furnace-registration-remove.js +6 -3
- package/dist/src/core/furnace-registration.js +9 -12
- package/dist/src/core/furnace-rollback.js +1 -1
- package/dist/src/core/furnace-scanner.js +2 -1
- package/dist/src/core/furnace-stale-export.js +2 -1
- package/dist/src/core/furnace-state-persist.d.ts +4 -6
- package/dist/src/core/furnace-validate-compatibility.js +4 -7
- package/dist/src/core/furnace-validate-helpers.js +33 -1
- package/dist/src/core/furnace-validate-registration.js +16 -0
- package/dist/src/core/furnace-validate.js +4 -2
- package/dist/src/core/git-diff.js +37 -2
- package/dist/src/core/lint-cache.d.ts +1 -1
- package/dist/src/core/lint-cache.js +33 -10
- package/dist/src/core/mach-build-artifacts.d.ts +63 -1
- package/dist/src/core/mach-build-artifacts.js +108 -2
- package/dist/src/core/mach-resource-shim.js +1 -0
- package/dist/src/core/marionette-port.d.ts +16 -0
- package/dist/src/core/marionette-port.js +93 -8
- package/dist/src/core/marionette-preflight.js +6 -5
- package/dist/src/core/patch-apply.d.ts +20 -13
- package/dist/src/core/patch-apply.js +32 -33
- package/dist/src/core/patch-export.js +3 -5
- package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
- package/dist/src/core/patch-lint-checkjs.js +78 -7
- package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
- package/dist/src/core/patch-lint-creators.d.ts +30 -0
- package/dist/src/core/patch-lint-creators.js +45 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -17
- package/dist/src/core/patch-lint-cross.js +16 -32
- package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
- package/dist/src/core/patch-lint-jsdoc.js +15 -5
- package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
- package/dist/src/core/patch-lint-module-registration.js +88 -0
- package/dist/src/core/patch-lint-mozbuild.js +99 -11
- package/dist/src/core/patch-lint-reexports.d.ts +1 -0
- package/dist/src/core/patch-lint-reexports.js +1 -0
- package/dist/src/core/patch-lint.d.ts +8 -0
- package/dist/src/core/patch-lint.js +84 -6
- package/dist/src/core/patch-lock.d.ts +12 -1
- package/dist/src/core/patch-lock.js +40 -4
- package/dist/src/core/patch-manifest-consistency.js +15 -3
- package/dist/src/core/patch-parse.d.ts +8 -0
- package/dist/src/core/patch-parse.js +10 -0
- package/dist/src/core/patch-policy.d.ts +7 -0
- package/dist/src/core/patch-policy.js +4 -1
- package/dist/src/core/patch-transform.d.ts +10 -0
- package/dist/src/core/patch-transform.js +13 -2
- package/dist/src/core/rebase-session.d.ts +44 -2
- package/dist/src/core/rebase-session.js +114 -17
- package/dist/src/core/register-browser-content.js +2 -1
- package/dist/src/core/register-shared-css.js +2 -1
- package/dist/src/core/smoke-patterns.d.ts +0 -8
- package/dist/src/core/smoke-patterns.js +3 -2
- package/dist/src/core/status-binary.d.ts +55 -0
- package/dist/src/core/status-binary.js +124 -0
- package/dist/src/core/status-classify.d.ts +6 -1
- package/dist/src/core/status-classify.js +101 -73
- package/dist/src/core/test-harness-crash.d.ts +32 -1
- package/dist/src/core/test-harness-crash.js +59 -4
- package/dist/src/core/test-path-scope.js +2 -0
- package/dist/src/core/test-stale-check.js +1 -20
- package/dist/src/core/toolchain-preflight.js +24 -26
- package/dist/src/core/tree-guard.d.ts +1 -1
- package/dist/src/core/tree-guard.js +67 -12
- package/dist/src/core/tree-store.d.ts +86 -10
- package/dist/src/core/tree-store.js +310 -72
- package/dist/src/core/typecheck.js +2 -1
- package/dist/src/core/wire-destroy.js +14 -3
- package/dist/src/core/wire-init.js +37 -40
- package/dist/src/core/wire-subscript.js +11 -3
- package/dist/src/core/wire-utils.js +4 -4
- package/dist/src/core/xpcshell-appdir.js +3 -0
- package/dist/src/errors/base.d.ts +13 -0
- package/dist/src/errors/base.js +13 -0
- package/dist/src/errors/rebase.d.ts +13 -0
- package/dist/src/errors/rebase.js +17 -0
- package/dist/src/types/commands/options.d.ts +86 -1
- package/dist/src/types/commands/patches.d.ts +8 -0
- package/dist/src/types/furnace.d.ts +7 -0
- package/dist/src/utils/build-info.d.ts +31 -0
- package/dist/src/utils/build-info.js +98 -0
- package/dist/src/utils/concurrency.d.ts +8 -0
- package/dist/src/utils/concurrency.js +21 -0
- package/dist/src/utils/errors.d.ts +39 -0
- package/dist/src/utils/errors.js +55 -0
- package/dist/src/utils/fs.js +5 -12
- package/dist/src/utils/logger.d.ts +14 -2
- package/dist/src/utils/logger.js +43 -13
- package/dist/src/utils/options.d.ts +21 -4
- package/dist/src/utils/options.js +26 -6
- package/dist/src/utils/package-root.js +7 -4
- package/dist/src/utils/process-group.js +13 -2
- package/dist/src/utils/stdio-drain.d.ts +28 -0
- package/dist/src/utils/stdio-drain.js +54 -0
- package/dist/src/utils/validation.d.ts +23 -2
- package/dist/src/utils/validation.js +29 -3
- package/package.json +9 -9
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* while reporting only the patch under export.
|
|
27
27
|
*/
|
|
28
28
|
import { basename, resolve } from 'node:path';
|
|
29
|
+
import { toError } from '../utils/errors.js';
|
|
29
30
|
import { pathExists } from '../utils/fs.js';
|
|
30
31
|
import { verbose } from '../utils/logger.js';
|
|
31
32
|
import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, TEST_HARNESS_SHIM, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
|
|
@@ -146,9 +147,17 @@ function createPathsResolver(ts, paths, baseDir, fileExists, onResolved) {
|
|
|
146
147
|
* the built-in Firefox-globals shim (from `patchLint.checkJsExtraShim`)
|
|
147
148
|
* @param projectRoot - Absolute project root for resolving `extraShimPath`
|
|
148
149
|
* @param mode - Strictness preset plus allowlisted compiler-option overrides
|
|
150
|
+
* @param builtinShimSuffix - Optional shim text appended AFTER the consumer shim
|
|
151
|
+
* @param rootScope - When set, only these repo-relative files become program
|
|
152
|
+
* ROOTS; resolution (and the host allowlist) still spans all of
|
|
153
|
+
* `resolutionOwned`, so a subset root's cross-patch imports type-check
|
|
154
|
+
* against the real owning sources while unrelated owned files are never
|
|
155
|
+
* parsed. `.mjs` files are module-scoped, so a root's diagnostics are
|
|
156
|
+
* identical whether other owned files are roots or mere resolution
|
|
157
|
+
* targets (pinned by the rootScope parity test — FORGE J1c).
|
|
149
158
|
* @returns Diagnostics grouped per owning file plus run-level errors
|
|
150
159
|
*/
|
|
151
|
-
export async function runCheckJsGrouped(repoDir, resolutionOwned, extraShimPath, projectRoot, mode, builtinShimSuffix) {
|
|
160
|
+
export async function runCheckJsGrouped(repoDir, resolutionOwned, extraShimPath, projectRoot, mode, builtinShimSuffix, rootScope) {
|
|
152
161
|
const empty = { byFile: new Map(), global: [] };
|
|
153
162
|
if (resolutionOwned.size === 0)
|
|
154
163
|
return empty;
|
|
@@ -171,14 +180,18 @@ export async function runCheckJsGrouped(repoDir, resolutionOwned, extraShimPath,
|
|
|
171
180
|
],
|
|
172
181
|
};
|
|
173
182
|
}
|
|
174
|
-
// Resolve absolute paths for root files, filtering to files that exist
|
|
183
|
+
// Resolve absolute paths for root files, filtering to files that exist.
|
|
184
|
+
// Under a rootScope, non-scoped owned files stay resolvable (host
|
|
185
|
+
// allowlist + relByAbsolute) but are not program roots — they are only
|
|
186
|
+
// parsed if a scoped root's import closure reaches them.
|
|
175
187
|
const rootFiles = [];
|
|
176
188
|
const ownedAbsolute = new Set();
|
|
177
189
|
const relByAbsolute = new Map();
|
|
178
190
|
for (const rel of resolutionOwned) {
|
|
179
191
|
const abs = resolve(repoDir, rel);
|
|
180
192
|
if (await pathExists(abs)) {
|
|
181
|
-
|
|
193
|
+
if (rootScope === undefined || rootScope.has(rel))
|
|
194
|
+
rootFiles.push(abs);
|
|
182
195
|
ownedAbsolute.add(abs);
|
|
183
196
|
relByAbsolute.set(abs, rel);
|
|
184
197
|
}
|
|
@@ -209,7 +222,7 @@ export async function runCheckJsGrouped(repoDir, resolutionOwned, extraShimPath,
|
|
|
209
222
|
{
|
|
210
223
|
file: extraShimPath ?? '(checkJs)',
|
|
211
224
|
check: 'checkjs-type-error',
|
|
212
|
-
message: err
|
|
225
|
+
message: toError(err).message,
|
|
213
226
|
severity: 'error',
|
|
214
227
|
},
|
|
215
228
|
],
|
|
@@ -423,7 +436,7 @@ export async function invokePatchLintCheckJs(repoDir, patchOwnedFiles, patchLint
|
|
|
423
436
|
* @param patchLint - The resolved `patchLint` config block
|
|
424
437
|
* @param projectRoot - FireForge project root for shim resolution
|
|
425
438
|
*/
|
|
426
|
-
export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint, projectRoot) {
|
|
439
|
+
export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint, projectRoot, rootScope) {
|
|
427
440
|
const merged = { byFile: new Map(), global: [] };
|
|
428
441
|
if (testFiles.size === 0)
|
|
429
442
|
return merged;
|
|
@@ -431,6 +444,11 @@ export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint,
|
|
|
431
444
|
const seenGlobal = new Set();
|
|
432
445
|
const files = [...testFiles].sort((a, b) => a.localeCompare(b));
|
|
433
446
|
for (const file of files) {
|
|
447
|
+
// Under a rootScope only the scoped files get their own program, but
|
|
448
|
+
// head.js helper discovery still spans the full owned set so a scoped
|
|
449
|
+
// test keeps its cross-patch harness globals (FORGE J1c).
|
|
450
|
+
if (rootScope !== undefined && !rootScope.has(file))
|
|
451
|
+
continue;
|
|
434
452
|
const dir = file.slice(0, file.lastIndexOf('/') + 1);
|
|
435
453
|
const roots = new Set([file]);
|
|
436
454
|
for (const candidate of files) {
|
|
@@ -479,7 +497,60 @@ function modeFromPatchLintConfig(patchLint) {
|
|
|
479
497
|
* @param patchOwnedFiles - Every patch-owned `.sys.mjs` in the queue
|
|
480
498
|
* @param patchLint - The resolved `patchLint` config block
|
|
481
499
|
* @param projectRoot - FireForge project root for shim resolution
|
|
500
|
+
* @param rootScope - Optional subset of files to use as program roots
|
|
501
|
+
* (`--patches`); resolution still spans the whole queue (FORGE J1c)
|
|
502
|
+
*/
|
|
503
|
+
export async function invokePatchLintCheckJsGrouped(repoDir, patchOwnedFiles, patchLint, projectRoot, rootScope) {
|
|
504
|
+
return runCheckJsGrouped(repoDir, patchOwnedFiles, patchLint.checkJsExtraShim, projectRoot, modeFromPatchLintConfig(patchLint), undefined, rootScope);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Cheap probe reproducing the only run-level ("global") checkJs findings
|
|
508
|
+
* the build path can produce — a missing `typescript` package and an
|
|
509
|
+
* unreadable consumer shim (`checkJsExtraShim`, and `checkJsTestShim`
|
|
510
|
+
* when `checkJsTestFiles` is on). The built program itself never
|
|
511
|
+
* contributes globals (see {@link runCheckJsGrouped}), so a warm
|
|
512
|
+
* all-cache-hit run can satisfy the "warm never reports less than cold"
|
|
513
|
+
* invariant (FORGE F5) with this probe instead of building the whole
|
|
514
|
+
* TypeScript program. Issue objects are byte-identical to the build
|
|
515
|
+
* path's, deduplicated by message like {@link runCheckJsTestFilesGrouped}.
|
|
482
516
|
*/
|
|
483
|
-
export async function
|
|
484
|
-
|
|
517
|
+
export async function probeCheckJsGlobalIssues(patchLint, projectRoot) {
|
|
518
|
+
try {
|
|
519
|
+
await import('typescript');
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
return [
|
|
523
|
+
{
|
|
524
|
+
file: '(checkJs)',
|
|
525
|
+
check: 'checkjs-type-error',
|
|
526
|
+
message: 'patchLint.checkJs is enabled but the "typescript" package is not installed. ' +
|
|
527
|
+
'Run "npm install typescript" to enable type checking.',
|
|
528
|
+
severity: 'error',
|
|
529
|
+
},
|
|
530
|
+
];
|
|
531
|
+
}
|
|
532
|
+
const issues = [];
|
|
533
|
+
const seen = new Set();
|
|
534
|
+
const probeShim = async (shimPath) => {
|
|
535
|
+
try {
|
|
536
|
+
await composeShimSource(projectRoot, shimPath);
|
|
537
|
+
}
|
|
538
|
+
catch (err) {
|
|
539
|
+
const message = toError(err).message;
|
|
540
|
+
if (seen.has(message))
|
|
541
|
+
return;
|
|
542
|
+
seen.add(message);
|
|
543
|
+
issues.push({
|
|
544
|
+
file: shimPath ?? '(checkJs)',
|
|
545
|
+
check: 'checkjs-type-error',
|
|
546
|
+
message,
|
|
547
|
+
severity: 'error',
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
await probeShim(patchLint.checkJsExtraShim);
|
|
552
|
+
if (patchLint.checkJsTestFiles === true) {
|
|
553
|
+
await probeShim(patchLint.checkJsTestShim);
|
|
554
|
+
}
|
|
555
|
+
return issues;
|
|
485
556
|
}
|
|
@@ -40,6 +40,15 @@ export function validateChromeScriptJsDoc(source, options) {
|
|
|
40
40
|
ast = parseScript(source, comments);
|
|
41
41
|
}
|
|
42
42
|
catch {
|
|
43
|
+
// Deliberate carve-out, unlike the `.sys.mjs` walker in
|
|
44
|
+
// `patch-lint-jsdoc.ts` (which reports a parse failure as an issue).
|
|
45
|
+
// `parseScript` rejects `import`/`export`, so a chrome subscript that was
|
|
46
|
+
// misclassified as a `.js` file — or that mistakenly uses module syntax —
|
|
47
|
+
// would emit a pseudo-issue for every rule here. The orchestrator already
|
|
48
|
+
// runs the export walker on `.sys.mjs` separately, so silently declining
|
|
49
|
+
// to lint an unparseable script is the correct degradation for this file
|
|
50
|
+
// and only this file. Pinned by "returns no issues when the source uses
|
|
51
|
+
// module-only syntax" in patch-lint-chrome-jsdoc.test.ts.
|
|
43
52
|
return [];
|
|
44
53
|
}
|
|
45
54
|
const issues = [];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* New-file creators map for the patch queue, split out of
|
|
3
|
+
* `patch-lint-cross.ts` (at the per-file line budget). Re-exported from
|
|
4
|
+
* there so callers keep importing from `patch-lint.ts` unchanged.
|
|
5
|
+
*
|
|
6
|
+
* Memoised per context object (FORGE J1): ownership resolution and
|
|
7
|
+
* cache-key fingerprints call this 2-3× per linted patch, and each call
|
|
8
|
+
* used to re-run `detectNewFilesInDiff` over EVERY queue entry's diff.
|
|
9
|
+
* Keyed weakly so a discarded context frees its map; a caller that
|
|
10
|
+
* mutates a context entry in place must call
|
|
11
|
+
* {@link invalidateNewFileCreatorsCache}.
|
|
12
|
+
*/
|
|
13
|
+
/** The slice of a queue context the creators map is derived from. */
|
|
14
|
+
interface CreatorsContext {
|
|
15
|
+
entries: {
|
|
16
|
+
filename: string;
|
|
17
|
+
diff: string;
|
|
18
|
+
}[];
|
|
19
|
+
}
|
|
20
|
+
/** Drops the memoised creators map after an in-place context entry update. */
|
|
21
|
+
export declare function invalidateNewFileCreatorsCache(ctx: CreatorsContext): void;
|
|
22
|
+
/**
|
|
23
|
+
* Returns the raw `path → patches[]` map of files created in `new file
|
|
24
|
+
* mode` by at least one patch in the queue. Paths created by only one
|
|
25
|
+
* patch are also included so callers can distinguish "no creator" from
|
|
26
|
+
* "exactly one creator" without re-scanning the diffs. The returned map
|
|
27
|
+
* is memoised and shared — treat it as read-only.
|
|
28
|
+
*/
|
|
29
|
+
export declare function collectNewFileCreatorsByPath(ctx: CreatorsContext): Map<string, string[]>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* New-file creators map for the patch queue, split out of
|
|
4
|
+
* `patch-lint-cross.ts` (at the per-file line budget). Re-exported from
|
|
5
|
+
* there so callers keep importing from `patch-lint.ts` unchanged.
|
|
6
|
+
*
|
|
7
|
+
* Memoised per context object (FORGE J1): ownership resolution and
|
|
8
|
+
* cache-key fingerprints call this 2-3× per linted patch, and each call
|
|
9
|
+
* used to re-run `detectNewFilesInDiff` over EVERY queue entry's diff.
|
|
10
|
+
* Keyed weakly so a discarded context frees its map; a caller that
|
|
11
|
+
* mutates a context entry in place must call
|
|
12
|
+
* {@link invalidateNewFileCreatorsCache}.
|
|
13
|
+
*/
|
|
14
|
+
import { detectNewFilesInDiff } from './patch-lint-diff.js';
|
|
15
|
+
const newFileCreatorsMemo = new WeakMap();
|
|
16
|
+
/** Drops the memoised creators map after an in-place context entry update. */
|
|
17
|
+
export function invalidateNewFileCreatorsCache(ctx) {
|
|
18
|
+
newFileCreatorsMemo.delete(ctx);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Returns the raw `path → patches[]` map of files created in `new file
|
|
22
|
+
* mode` by at least one patch in the queue. Paths created by only one
|
|
23
|
+
* patch are also included so callers can distinguish "no creator" from
|
|
24
|
+
* "exactly one creator" without re-scanning the diffs. The returned map
|
|
25
|
+
* is memoised and shared — treat it as read-only.
|
|
26
|
+
*/
|
|
27
|
+
export function collectNewFileCreatorsByPath(ctx) {
|
|
28
|
+
const memoised = newFileCreatorsMemo.get(ctx);
|
|
29
|
+
if (memoised)
|
|
30
|
+
return memoised;
|
|
31
|
+
const creators = new Map();
|
|
32
|
+
for (const entry of ctx.entries) {
|
|
33
|
+
const newFiles = detectNewFilesInDiff(entry.diff);
|
|
34
|
+
for (const file of newFiles) {
|
|
35
|
+
let owners = creators.get(file);
|
|
36
|
+
if (!owners) {
|
|
37
|
+
owners = [];
|
|
38
|
+
creators.set(file, owners);
|
|
39
|
+
}
|
|
40
|
+
owners.push(entry.filename);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
newFileCreatorsMemo.set(ctx, creators);
|
|
44
|
+
return creators;
|
|
45
|
+
}
|
|
@@ -79,23 +79,7 @@ export interface PatchQueueContext {
|
|
|
79
79
|
* @param patchesDir - Path to the patches directory
|
|
80
80
|
*/
|
|
81
81
|
export declare function buildPatchQueueContext(patchesDir: string, config?: FireForgeConfig): Promise<PatchQueueContext>;
|
|
82
|
-
|
|
83
|
-
* Returns the raw `path → patches[]` map of files created in `new file
|
|
84
|
-
* mode` by at least one patch in the queue. Paths created by only one
|
|
85
|
-
* patch are also included so callers can distinguish "no creator" from
|
|
86
|
-
* "exactly one creator" without re-scanning the diffs.
|
|
87
|
-
*
|
|
88
|
-
* Split out from {@link lintPatchQueueDuplicateCreations} so
|
|
89
|
-
* `status --ownership` (and any future caller that wants ownership
|
|
90
|
-
* without a rendered PatchLintIssue) can consume the same structured
|
|
91
|
-
* data the rule itself relies on. Previously status had to parse the
|
|
92
|
-
* rule's human-readable message to recover the patch list, which was
|
|
93
|
-
* both fragile and made the lint message format part of an implicit
|
|
94
|
-
* contract.
|
|
95
|
-
*
|
|
96
|
-
* @param ctx - Pre-built queue context
|
|
97
|
-
*/
|
|
98
|
-
export declare function collectNewFileCreatorsByPath(ctx: PatchQueueContext): Map<string, string[]>;
|
|
82
|
+
export { collectNewFileCreatorsByPath, invalidateNewFileCreatorsCache, } from './patch-lint-creators.js';
|
|
99
83
|
/**
|
|
100
84
|
* Cross-patch lint rule: the same path is newly created (`--- /dev/null →
|
|
101
85
|
* +++ b/path`) by more than one patch. This is the failure mode that
|
|
@@ -16,7 +16,9 @@ import { readText } from '../utils/fs.js';
|
|
|
16
16
|
import { verbose } from '../utils/logger.js';
|
|
17
17
|
import { stripJsComments } from '../utils/regex.js';
|
|
18
18
|
import { discoverPatches } from './patch-files.js';
|
|
19
|
+
import { collectNewFileCreatorsByPath } from './patch-lint-creators.js';
|
|
19
20
|
import { detectNewFilesInDiff, extractAddedLinesPerFile } from './patch-lint-diff.js';
|
|
21
|
+
import { lintPatchQueueModuleRegistrations } from './patch-lint-module-registration.js';
|
|
20
22
|
import { loadPatchesManifest } from './patch-manifest-io.js';
|
|
21
23
|
import { categoryRangeForOrder, categoryRangeLabel } from './patch-policy.js';
|
|
22
24
|
import { extractNewFileContent } from './patch-transform.js';
|
|
@@ -81,37 +83,11 @@ export async function buildPatchQueueContext(patchesDir, config) {
|
|
|
81
83
|
entries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
82
84
|
return { entries, ...(config?.patchPolicy ? { patchPolicy: config.patchPolicy } : {}) };
|
|
83
85
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
*
|
|
90
|
-
* Split out from {@link lintPatchQueueDuplicateCreations} so
|
|
91
|
-
* `status --ownership` (and any future caller that wants ownership
|
|
92
|
-
* without a rendered PatchLintIssue) can consume the same structured
|
|
93
|
-
* data the rule itself relies on. Previously status had to parse the
|
|
94
|
-
* rule's human-readable message to recover the patch list, which was
|
|
95
|
-
* both fragile and made the lint message format part of an implicit
|
|
96
|
-
* contract.
|
|
97
|
-
*
|
|
98
|
-
* @param ctx - Pre-built queue context
|
|
99
|
-
*/
|
|
100
|
-
export function collectNewFileCreatorsByPath(ctx) {
|
|
101
|
-
const creators = new Map();
|
|
102
|
-
for (const entry of ctx.entries) {
|
|
103
|
-
const newFiles = detectNewFilesInDiff(entry.diff);
|
|
104
|
-
for (const file of newFiles) {
|
|
105
|
-
let owners = creators.get(file);
|
|
106
|
-
if (!owners) {
|
|
107
|
-
owners = [];
|
|
108
|
-
creators.set(file, owners);
|
|
109
|
-
}
|
|
110
|
-
owners.push(entry.filename);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return creators;
|
|
114
|
-
}
|
|
86
|
+
// The creators map (split out of `lintPatchQueueDuplicateCreations` so
|
|
87
|
+
// `status --ownership` can consume the structured data) lives in
|
|
88
|
+
// `patch-lint-creators.ts` with its per-context memoisation (FORGE J1);
|
|
89
|
+
// re-exported here so callers keep importing from `patch-lint.ts`.
|
|
90
|
+
export { collectNewFileCreatorsByPath, invalidateNewFileCreatorsCache, } from './patch-lint-creators.js';
|
|
115
91
|
/**
|
|
116
92
|
* Cross-patch lint rule: the same path is newly created (`--- /dev/null →
|
|
117
93
|
* +++ b/path`) by more than one patch. This is the failure mode that
|
|
@@ -131,6 +107,7 @@ export function lintPatchQueueDuplicateCreations(ctx) {
|
|
|
131
107
|
issues.push({
|
|
132
108
|
file,
|
|
133
109
|
check: 'duplicate-new-file-creation',
|
|
110
|
+
patches: [...owners].sort((a, b) => a.localeCompare(b)),
|
|
134
111
|
fingerprint: `duplicate-new-file-creation|${file}|${[...owners].sort((a, b) => a.localeCompare(b)).join(',')}`,
|
|
135
112
|
message: `File "${file}" is created (new file mode) by multiple patches: ${owners.join(', ')}. ` +
|
|
136
113
|
'Only one patch may create a given path. Use "patch delete" or ' +
|
|
@@ -519,6 +496,7 @@ export function lintPatchQueueForwardImports(ctx) {
|
|
|
519
496
|
issues.push({
|
|
520
497
|
file: sitePath,
|
|
521
498
|
check: 'forward-import',
|
|
499
|
+
patches: [entry.filename],
|
|
522
500
|
fingerprint: `forward-import|${sitePath}|${cleaned}|${fingerprintOwners}`,
|
|
523
501
|
message: `${sitePath} in ${entry.filename} imports "${specifier}", ` +
|
|
524
502
|
`but the matching new file is created by a later patch: ${ownersSummary}. ` +
|
|
@@ -537,6 +515,7 @@ export function lintPatchQueueForwardImports(ctx) {
|
|
|
537
515
|
issues.push({
|
|
538
516
|
file: dependency.file,
|
|
539
517
|
check: 'staged-dependency-unused',
|
|
518
|
+
patches: [entry.filename],
|
|
540
519
|
fingerprint: `staged-dependency-unused|${entry.filename}|${dependency.file}|${dependency.specifier}|${dependency.creates}|${dependency.owner ?? ''}`,
|
|
541
520
|
message: `${entry.filename} declares a staged forward import from ${dependency.file} ` +
|
|
542
521
|
`to "${dependency.specifier}" / ${dependency.creates}, but that exact forward dependency was not found. ` +
|
|
@@ -559,6 +538,7 @@ export function lintPatchQueueForwardImports(ctx) {
|
|
|
559
538
|
issues.push({
|
|
560
539
|
file: registration.file,
|
|
561
540
|
check: 'staged-dependency-unused',
|
|
541
|
+
patches: [entry.filename],
|
|
562
542
|
fingerprint: `staged-dependency-unused|${entry.filename}|${registration.file}|reg:${registration.line}|${registration.creates}|${registration.owner ?? ''}`,
|
|
563
543
|
message: `${entry.filename} declares a staged registration in ${registration.file} ` +
|
|
564
544
|
`("${registration.line}") for ${registration.creates}, but ${describeRegistrationFailure(failure)} ` +
|
|
@@ -631,5 +611,9 @@ function isRegistrationLinePresent(entry, registration) {
|
|
|
631
611
|
* manually for projected/hypothetical states)
|
|
632
612
|
*/
|
|
633
613
|
export function lintPatchQueue(ctx) {
|
|
634
|
-
return [
|
|
614
|
+
return [
|
|
615
|
+
...lintPatchQueueDuplicateCreations(ctx),
|
|
616
|
+
...lintPatchQueueForwardImports(ctx),
|
|
617
|
+
...lintPatchQueueModuleRegistrations(ctx),
|
|
618
|
+
];
|
|
635
619
|
}
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
import type * as acorn from 'acorn';
|
|
10
10
|
import type { ClassDeclaration, FunctionDeclaration } from 'estree';
|
|
11
11
|
import type { AcornESTreeNode } from './ast-utils.js';
|
|
12
|
-
export type JsDocCheck =
|
|
12
|
+
export type JsDocCheck =
|
|
13
|
+
/** The source could not be parsed, so no JSDoc rule could run on it. */
|
|
14
|
+
'jsdoc-unparseable-source' | 'missing-jsdoc' | 'jsdoc-param-mismatch' | 'jsdoc-missing-returns' | 'missing-jsdoc-class-method' | 'jsdoc-class-method-param-mismatch' | 'jsdoc-class-method-missing-returns';
|
|
13
15
|
export interface JsDocIssue {
|
|
14
16
|
line: number;
|
|
15
17
|
check: JsDocCheck;
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* Separated from `patch-lint.ts` to keep both files within the
|
|
8
8
|
* project's per-file line budget.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { toError } from '../utils/errors.js';
|
|
11
|
+
import { asEstree, parseModule } from './ast-utils.js';
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// JSDoc comment helpers
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
@@ -339,7 +340,7 @@ export function validateClassMethods(cls, comments, source, issues, severity) {
|
|
|
339
340
|
for (const member of cls.body.body) {
|
|
340
341
|
if (member.type !== 'MethodDefinition')
|
|
341
342
|
continue;
|
|
342
|
-
const method = member;
|
|
343
|
+
const method = asEstree(member);
|
|
343
344
|
if (isPrivateMethodKey(method))
|
|
344
345
|
continue;
|
|
345
346
|
const name = staticMethodName(method);
|
|
@@ -369,7 +370,7 @@ export function validateClassMethods(cls, comments, source, issues, severity) {
|
|
|
369
370
|
continue;
|
|
370
371
|
}
|
|
371
372
|
const skipReturns = method.kind === 'constructor' || method.kind === 'set';
|
|
372
|
-
validateParamsAndReturns(method.value, jsDoc, issues, {
|
|
373
|
+
validateParamsAndReturns(asEstree(method.value), jsDoc, issues, {
|
|
373
374
|
label,
|
|
374
375
|
line,
|
|
375
376
|
paramCheck: 'jsdoc-class-method-param-mismatch',
|
|
@@ -396,8 +397,17 @@ export function validateExportJsDoc(source, options) {
|
|
|
396
397
|
try {
|
|
397
398
|
ast = parseModule(source, comments);
|
|
398
399
|
}
|
|
399
|
-
catch {
|
|
400
|
-
|
|
400
|
+
catch (error) {
|
|
401
|
+
// An unparseable source is NOT a clean file. Returning `[]` here was the
|
|
402
|
+
// same value as "fully documented", so a syntax error silently cleared
|
|
403
|
+
// every rule this module enforces. Report it instead.
|
|
404
|
+
return [
|
|
405
|
+
{
|
|
406
|
+
line: 1,
|
|
407
|
+
check: 'jsdoc-unparseable-source',
|
|
408
|
+
message: `Source could not be parsed for JSDoc analysis: ${toError(error).message}`,
|
|
409
|
+
},
|
|
410
|
+
];
|
|
401
411
|
}
|
|
402
412
|
const issues = [];
|
|
403
413
|
const body = ast.body;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
2
|
+
interface ModuleRegistrationQueueEntry {
|
|
3
|
+
filename: string;
|
|
4
|
+
newFiles: ReadonlyMap<string, string>;
|
|
5
|
+
modifiedFileAdditions: ReadonlyMap<string, string>;
|
|
6
|
+
}
|
|
7
|
+
interface ModuleRegistrationQueueContext {
|
|
8
|
+
entries: readonly ModuleRegistrationQueueEntry[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Flags a new `.sys.mjs` imported through a Firefox resource URL when the
|
|
12
|
+
* projected queue never adds it to a moz.build list.
|
|
13
|
+
*/
|
|
14
|
+
export declare function lintPatchQueueModuleRegistrations(ctx: ModuleRegistrationQueueContext): PatchLintIssue[];
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Queue-wide registration lint for newly-created Firefox system modules.
|
|
4
|
+
*/
|
|
5
|
+
import { basename } from 'node:path';
|
|
6
|
+
import { escapeRegex, stripJsComments } from '../utils/regex.js';
|
|
7
|
+
const IMPORTABLE_EXTENSIONS = ['.mjs', '.sys.mjs', '.js', '.jsm'];
|
|
8
|
+
/** Maps Firefox's common system-module resource URLs to their source suffix. */
|
|
9
|
+
function moduleSourceSuffix(specifier) {
|
|
10
|
+
const cleaned = specifier.split(/[?#]/)[0] ?? specifier;
|
|
11
|
+
for (const prefix of ['resource:///modules/', 'resource://gre/modules/']) {
|
|
12
|
+
if (cleaned.startsWith(prefix))
|
|
13
|
+
return `/modules/${cleaned.slice(prefix.length)}`;
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
/** Resource-module strings in actual code (comments removed), de-duplicated. */
|
|
18
|
+
function extractResourceModuleSpecifiers(content) {
|
|
19
|
+
const stripped = stripJsComments(content);
|
|
20
|
+
const found = new Set();
|
|
21
|
+
const pattern = /["'](resource:\/\/(?:\/|gre\/)modules\/[^"']+\.sys\.mjs(?:[?#][^"']*)?)["']/g;
|
|
22
|
+
for (const match of stripped.matchAll(pattern)) {
|
|
23
|
+
if (match[1] !== undefined)
|
|
24
|
+
found.add(match[1]);
|
|
25
|
+
}
|
|
26
|
+
return [...found];
|
|
27
|
+
}
|
|
28
|
+
/** True when some patch in the projected queue adds the module to moz.build. */
|
|
29
|
+
function queueRegistersModule(ctx, leaf) {
|
|
30
|
+
const escaped = escapeRegex(leaf);
|
|
31
|
+
const quotedLeaf = new RegExp(`["']${escaped}["']`);
|
|
32
|
+
for (const entry of ctx.entries) {
|
|
33
|
+
for (const [path, content] of [...entry.newFiles, ...entry.modifiedFileAdditions]) {
|
|
34
|
+
if ((path === 'moz.build' || path.endsWith('/moz.build')) && quotedLeaf.test(content)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Flags a new `.sys.mjs` imported through a Firefox resource URL when the
|
|
43
|
+
* projected queue never adds it to a moz.build list.
|
|
44
|
+
*/
|
|
45
|
+
export function lintPatchQueueModuleRegistrations(ctx) {
|
|
46
|
+
const imported = new Map();
|
|
47
|
+
const collect = (entry, path, content) => {
|
|
48
|
+
if (!IMPORTABLE_EXTENSIONS.some((extension) => path.endsWith(extension)))
|
|
49
|
+
return;
|
|
50
|
+
for (const specifier of extractResourceModuleSpecifiers(content)) {
|
|
51
|
+
const suffix = moduleSourceSuffix(specifier);
|
|
52
|
+
if (suffix === undefined)
|
|
53
|
+
continue;
|
|
54
|
+
const importers = imported.get(suffix) ?? new Set();
|
|
55
|
+
importers.add(entry.filename);
|
|
56
|
+
imported.set(suffix, importers);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
for (const entry of ctx.entries) {
|
|
60
|
+
for (const [path, content] of entry.newFiles)
|
|
61
|
+
collect(entry, path, content);
|
|
62
|
+
for (const [path, content] of entry.modifiedFileAdditions)
|
|
63
|
+
collect(entry, path, content);
|
|
64
|
+
}
|
|
65
|
+
const issues = [];
|
|
66
|
+
for (const entry of ctx.entries) {
|
|
67
|
+
for (const path of entry.newFiles.keys()) {
|
|
68
|
+
if (!path.endsWith('.sys.mjs'))
|
|
69
|
+
continue;
|
|
70
|
+
const matched = [...imported.entries()].find(([suffix]) => path.endsWith(suffix));
|
|
71
|
+
if (!matched || queueRegistersModule(ctx, basename(path)))
|
|
72
|
+
continue;
|
|
73
|
+
const [suffix, importers] = matched;
|
|
74
|
+
issues.push({
|
|
75
|
+
file: path,
|
|
76
|
+
check: 'unregistered-system-module',
|
|
77
|
+
patches: [...new Set([entry.filename, ...importers])],
|
|
78
|
+
fingerprint: `unregistered-system-module|${path}|${suffix}`,
|
|
79
|
+
message: `${path} is newly created and imported as a resource module, but no patch adds ` +
|
|
80
|
+
`${basename(path)} to a moz.build list. Register it in EXTRA_JS_MODULES (or the ` +
|
|
81
|
+
'appropriate namespaced list) before export; source-level typechecks can resolve the ' +
|
|
82
|
+
'file even though the packaged browser cannot.',
|
|
83
|
+
severity: 'error',
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return issues;
|
|
88
|
+
}
|
|
@@ -31,40 +31,128 @@ const STRICT_ORDERED_MOZBUILD_LISTS = [
|
|
|
31
31
|
'XPIDL_SOURCES',
|
|
32
32
|
];
|
|
33
33
|
const LIST_OPENER = new RegExp(`^\\s*((?:${STRICT_ORDERED_MOZBUILD_LISTS.join('|')})(?:\\.[A-Za-z_][A-Za-z0-9_]*)*)\\s*\\+?=\\s*\\[`);
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* Strips a Python trailing comment from a moz.build line.
|
|
36
|
+
*
|
|
37
|
+
* `#` inside a quoted string is not a comment, so the scan tracks quote state
|
|
38
|
+
* rather than using `indexOf('#')`.
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* True when `line` contains a `]` outside any quoted span. A bracket inside an
|
|
42
|
+
* item (`"icons[2x].png"`) is filename text, not the list's close — reading it
|
|
43
|
+
* as one truncated the item set and left every later item unchecked.
|
|
44
|
+
*/
|
|
45
|
+
function hasUnquotedCloseBracket(line) {
|
|
46
|
+
let quote;
|
|
47
|
+
for (let i = 0; i < line.length; i++) {
|
|
48
|
+
const ch = line[i];
|
|
49
|
+
if (quote !== undefined) {
|
|
50
|
+
if (ch === '\\')
|
|
51
|
+
i += 1;
|
|
52
|
+
else if (ch === quote)
|
|
53
|
+
quote = undefined;
|
|
54
|
+
}
|
|
55
|
+
else if (ch === '"' || ch === "'") {
|
|
56
|
+
quote = ch;
|
|
57
|
+
}
|
|
58
|
+
else if (ch === ']') {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
function stripMozBuildComment(line) {
|
|
65
|
+
let quote;
|
|
66
|
+
for (let i = 0; i < line.length; i++) {
|
|
67
|
+
const ch = line[i];
|
|
68
|
+
if (quote !== undefined) {
|
|
69
|
+
if (ch === '\\')
|
|
70
|
+
i += 1;
|
|
71
|
+
else if (ch === quote)
|
|
72
|
+
quote = undefined;
|
|
73
|
+
}
|
|
74
|
+
else if (ch === '"' || ch === "'") {
|
|
75
|
+
quote = ch;
|
|
76
|
+
}
|
|
77
|
+
else if (ch === '#') {
|
|
78
|
+
return line.slice(0, i);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return line;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Extracts every strict-ordered list (with items in file order) from moz.build
|
|
85
|
+
* content.
|
|
86
|
+
*
|
|
87
|
+
* Comments are stripped before both the item scan and the close-bracket test.
|
|
88
|
+
* moz.build is Python, and before 0.41.0 neither step knew that:
|
|
89
|
+
*
|
|
90
|
+
* - a quoted string in a trailing comment was scraped in as a phantom list
|
|
91
|
+
* item (`"Zeta.cpp", # replaces "Beta.cpp"` yielded a `Beta.cpp` entry), so
|
|
92
|
+
* the sort check reported an unsorted list naming a file that is not in it —
|
|
93
|
+
* with a fingerprint that could never stabilise, because the item does not
|
|
94
|
+
* exist to be moved;
|
|
95
|
+
* - a `]` anywhere in a comment (`# see foo[0]`) closed the list early and
|
|
96
|
+
* silently truncated the item set.
|
|
97
|
+
*
|
|
98
|
+
* The scan also no longer advances the caller's loop counter: an unterminated
|
|
99
|
+
* list used to consume the rest of the file, so every *later* list in it was
|
|
100
|
+
* skipped entirely.
|
|
101
|
+
*
|
|
102
|
+
* Nor does an unterminated list borrow the NEXT list's bracket. The forward
|
|
103
|
+
* scan stops at the next list opener rather than reading through it: without
|
|
104
|
+
* that, an unclosed `EXTRA_COMPONENTS` swallowed the following
|
|
105
|
+
* `EXTRA_JS_MODULES` — its items merged into the first list's item set and its
|
|
106
|
+
* `]` accepted as the first list's close — reporting a sorting error against a
|
|
107
|
+
* variable that never contained those items, and skipping the second list.
|
|
108
|
+
*/
|
|
35
109
|
function collectStrictOrderedLists(content) {
|
|
36
110
|
const lines = content.split('\n');
|
|
37
111
|
const occurrences = [];
|
|
38
112
|
for (let i = 0; i < lines.length; i++) {
|
|
39
|
-
const
|
|
40
|
-
const opener = LIST_OPENER.exec(line);
|
|
113
|
+
const opener = LIST_OPENER.exec(lines[i] ?? '');
|
|
41
114
|
if (!opener)
|
|
42
115
|
continue;
|
|
43
116
|
const varName = opener[1] ?? '';
|
|
44
117
|
const items = [];
|
|
45
118
|
// Items may start on the opener line (`VAR += ["a", "b"]`) or span
|
|
46
119
|
// following lines until the closing bracket.
|
|
47
|
-
let rest =
|
|
120
|
+
let rest = stripMozBuildComment((lines[i] ?? '').slice(opener[0].length));
|
|
48
121
|
let closed = false;
|
|
122
|
+
// Scan ahead on a local cursor so an unterminated list costs only itself.
|
|
123
|
+
let cursor = i;
|
|
49
124
|
for (;;) {
|
|
50
|
-
|
|
51
|
-
|
|
125
|
+
// Quote types are matched pairwise so an apostrophe inside a
|
|
126
|
+
// double-quoted item ("don't.cpp") cannot terminate the match early and
|
|
127
|
+
// scrape in a phantom item.
|
|
128
|
+
for (const quoted of rest.matchAll(/"([^"]+)"|'([^']+)'/g)) {
|
|
129
|
+
items.push(quoted[1] ?? quoted[2] ?? '');
|
|
52
130
|
}
|
|
53
|
-
if (
|
|
131
|
+
if (hasUnquotedCloseBracket(rest)) {
|
|
54
132
|
closed = true;
|
|
55
133
|
break;
|
|
56
134
|
}
|
|
57
|
-
|
|
58
|
-
if (
|
|
135
|
+
cursor += 1;
|
|
136
|
+
if (cursor >= lines.length)
|
|
59
137
|
break;
|
|
60
|
-
rest = lines[
|
|
138
|
+
rest = stripMozBuildComment(lines[cursor] ?? '');
|
|
139
|
+
if (LIST_OPENER.test(rest)) {
|
|
140
|
+
// A new list starts here, so the previous one never closed. Rewind so
|
|
141
|
+
// the outer loop examines this line as its own opener.
|
|
142
|
+
cursor -= 1;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
61
145
|
if (/^\s*\]/.test(rest)) {
|
|
62
146
|
closed = true;
|
|
63
147
|
break;
|
|
64
148
|
}
|
|
65
149
|
}
|
|
66
|
-
if (closed)
|
|
150
|
+
if (closed) {
|
|
67
151
|
occurrences.push({ varName, items });
|
|
152
|
+
// Resume after the list we just consumed; a list that never closed
|
|
153
|
+
// leaves `i` untouched so the next line is still examined.
|
|
154
|
+
i = cursor;
|
|
155
|
+
}
|
|
68
156
|
}
|
|
69
157
|
return occurrences;
|
|
70
158
|
}
|