@hominis/fireforge 0.47.0 → 0.48.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 +65 -0
- package/dist/bin/fireforge.js +11 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
- package/dist/src/commands/doctor-macos-sdk.js +156 -0
- package/dist/src/commands/doctor.js +2 -0
- package/dist/src/commands/download.js +64 -3
- package/dist/src/commands/import.js +5 -2
- package/dist/src/commands/lint-per-patch.js +44 -6
- package/dist/src/commands/lint-per-run-checkjs.d.ts +3 -0
- package/dist/src/commands/lint-per-run-checkjs.js +1 -0
- package/dist/src/commands/lint.js +10 -0
- package/dist/src/commands/re-export-lint.d.ts +17 -2
- package/dist/src/commands/re-export-lint.js +33 -4
- package/dist/src/commands/re-export.js +4 -2
- package/dist/src/commands/rebase/confirm.d.ts +12 -1
- package/dist/src/commands/rebase/confirm.js +6 -3
- package/dist/src/commands/rebase/index.js +77 -13
- package/dist/src/commands/run.js +43 -7
- package/dist/src/commands/status.js +5 -3
- package/dist/src/commands/test-harness-teardown.d.ts +62 -0
- package/dist/src/commands/test-harness-teardown.js +128 -0
- package/dist/src/commands/test-register.js +2 -1
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +36 -7
- package/dist/src/commands/test-verdict.d.ts +19 -4
- package/dist/src/commands/test-verdict.js +22 -4
- package/dist/src/commands/test.js +18 -10
- package/dist/src/commands/typecheck.d.ts +5 -0
- package/dist/src/commands/typecheck.js +7 -1
- package/dist/src/core/build-prepare.d.ts +2 -0
- package/dist/src/core/build-prepare.js +67 -35
- 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 +7 -0
- package/dist/src/core/config-validate.js +7 -0
- package/dist/src/core/discard-baseline.js +2 -1
- package/dist/src/core/file-lock.js +16 -9
- package/dist/src/core/forward-registrations.d.ts +33 -0
- package/dist/src/core/forward-registrations.js +16 -0
- package/dist/src/core/git-file-ops.d.ts +21 -0
- package/dist/src/core/git-file-ops.js +75 -0
- package/dist/src/core/git-status.d.ts +14 -0
- package/dist/src/core/git-status.js +41 -2
- package/dist/src/core/harness-helper-pids.d.ts +98 -0
- package/dist/src/core/harness-helper-pids.js +240 -0
- package/dist/src/core/harness-orphans.d.ts +53 -8
- package/dist/src/core/harness-orphans.js +125 -44
- package/dist/src/core/mach-mozconfig.d.ts +2 -1
- package/dist/src/core/mach-mozconfig.js +2 -0
- package/dist/src/core/mach.d.ts +16 -0
- package/dist/src/core/mach.js +6 -1
- package/dist/src/core/marionette-preflight.js +9 -3
- package/dist/src/core/parent-exit-watchdog.d.ts +62 -0
- package/dist/src/core/parent-exit-watchdog.js +69 -0
- package/dist/src/core/patch-apply-fuzz.d.ts +12 -0
- package/dist/src/core/patch-apply-fuzz.js +27 -6
- package/dist/src/core/patch-apply.d.ts +13 -0
- package/dist/src/core/patch-apply.js +27 -2
- package/dist/src/core/patch-lint-checkjs-cache.d.ts +18 -0
- package/dist/src/core/patch-lint-checkjs-cache.js +45 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -0
- package/dist/src/core/patch-lint-checkjs.js +8 -1
- package/dist/src/core/patch-lint-cross.d.ts +7 -1
- package/dist/src/core/patch-lint-cross.js +13 -12
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +38 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +240 -0
- package/dist/src/core/patch-lint-forward-registration.d.ts +18 -5
- package/dist/src/core/patch-lint-forward-registration.js +83 -37
- package/dist/src/core/patch-lint.d.ts +11 -0
- package/dist/src/core/patch-lint.js +20 -2
- package/dist/src/core/patch-transform.d.ts +14 -13
- package/dist/src/core/patch-transform.js +29 -17
- package/dist/src/core/rebase-dry-run.d.ts +48 -0
- package/dist/src/core/rebase-dry-run.js +105 -0
- package/dist/src/core/run-profile.d.ts +50 -0
- package/dist/src/core/run-profile.js +131 -0
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +3 -1
- package/dist/src/core/status-classify.js +29 -2
- package/dist/src/core/toolchain-preflight.d.ts +6 -0
- package/dist/src/core/toolchain-preflight.js +1 -1
- package/dist/src/core/typecheck.d.ts +11 -1
- package/dist/src/core/typecheck.js +85 -11
- package/dist/src/errors/rebase.d.ts +9 -0
- package/dist/src/errors/rebase.js +20 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -1
- package/dist/src/types/commands/options.d.ts +26 -1
- package/dist/src/types/config.d.ts +17 -0
- package/dist/src/utils/process-group.d.ts +5 -1
- package/dist/src/utils/process-group.js +7 -3
- package/dist/src/utils/process-input.d.ts +27 -0
- package/dist/src/utils/process-input.js +49 -0
- package/dist/src/utils/process.d.ts +20 -0
- package/dist/src/utils/process.js +16 -3
- package/dist/src/utils/validation.d.ts +11 -0
- package/dist/src/utils/validation.js +43 -0
- package/docs/configuration.md +88 -1
- package/docs/exit-codes.md +1 -1
- package/docs/lifecycle-invariants.md +21 -4
- package/docs/machine-output.md +22 -9
- package/docs/patch-workflow.md +35 -1
- package/docs/testing.md +68 -4
- package/package.json +7 -3
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsed `SourceFile`s reused across programs, keyed by file name and the
|
|
3
|
+
* parse options TypeScript asked for. The language service shares files
|
|
4
|
+
* across programs the same way (its document registry): a `SourceFile` is
|
|
5
|
+
* parsed and bound once, and every program that includes it reuses both.
|
|
6
|
+
* Only valid while the files cannot change and every program uses the same
|
|
7
|
+
* compiler options, which holds within one lint run.
|
|
8
|
+
*/
|
|
9
|
+
export type CheckJsSourceFileCache = Map<string, import('typescript').SourceFile>;
|
|
10
|
+
/**
|
|
11
|
+
* Returns `host` with a shared-parse `getSourceFile` when a cache is given,
|
|
12
|
+
* or `host` itself when not.
|
|
13
|
+
* @param host - The program's compiler host
|
|
14
|
+
* @param cache - Shared parse cache, or undefined for an unshared program
|
|
15
|
+
* @param shimPath - Path the host serves the composed shim at
|
|
16
|
+
* @param shimSource - The composed shim text
|
|
17
|
+
*/
|
|
18
|
+
export declare function withSharedSourceFiles(host: import('typescript').CompilerHost, cache: CheckJsSourceFileCache | undefined, shimPath: string, shimSource: string): import('typescript').CompilerHost;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Parsed-source sharing across checkJs programs.
|
|
4
|
+
*
|
|
5
|
+
* Its own module rather than part of `patch-lint-checkjs.ts` only because
|
|
6
|
+
* that file sits on a 500-line budget.
|
|
7
|
+
*/
|
|
8
|
+
import { sha256Hex } from '../utils/hash.js';
|
|
9
|
+
/**
|
|
10
|
+
* Wraps a host's `getSourceFile` so each file is parsed at most once per
|
|
11
|
+
* cache. The shim is keyed by its content as well, since the shim text is
|
|
12
|
+
* composed per call rather than read from disk.
|
|
13
|
+
*/
|
|
14
|
+
function cachingGetSourceFile(cache, shimPath, shimSource, getSourceFile) {
|
|
15
|
+
const shimDigest = sha256Hex(shimSource);
|
|
16
|
+
return (fileName, languageVersionOrOptions, onError, shouldCreate) => {
|
|
17
|
+
const parse = typeof languageVersionOrOptions === 'object'
|
|
18
|
+
? `${languageVersionOrOptions.languageVersion}|${String(languageVersionOrOptions.impliedNodeFormat)}`
|
|
19
|
+
: String(languageVersionOrOptions);
|
|
20
|
+
const key = fileName === shimPath ? `${fileName}\0${parse}\0${shimDigest}` : `${fileName}\0${parse}`;
|
|
21
|
+
const cached = cache.get(key);
|
|
22
|
+
if (cached !== undefined)
|
|
23
|
+
return cached;
|
|
24
|
+
const created = getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreate);
|
|
25
|
+
if (created !== undefined)
|
|
26
|
+
cache.set(key, created);
|
|
27
|
+
return created;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns `host` with a shared-parse `getSourceFile` when a cache is given,
|
|
32
|
+
* or `host` itself when not.
|
|
33
|
+
* @param host - The program's compiler host
|
|
34
|
+
* @param cache - Shared parse cache, or undefined for an unshared program
|
|
35
|
+
* @param shimPath - Path the host serves the composed shim at
|
|
36
|
+
* @param shimSource - The composed shim text
|
|
37
|
+
*/
|
|
38
|
+
export function withSharedSourceFiles(host, cache, shimPath, shimSource) {
|
|
39
|
+
if (cache === undefined)
|
|
40
|
+
return host;
|
|
41
|
+
return {
|
|
42
|
+
...host,
|
|
43
|
+
getSourceFile: cachingGetSourceFile(cache, shimPath, shimSource, host.getSourceFile.bind(host)),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
28
28
|
import type { PatchLintCheckJsCompilerOptions, PatchLintConfig, PatchLintSeverityGate } from '../types/config.js';
|
|
29
|
+
import { type CheckJsSourceFileCache } from './patch-lint-checkjs-cache.js';
|
|
29
30
|
/** How a checkJs run controls reporting and resolution. See module docs. */
|
|
30
31
|
export interface CheckJsMode {
|
|
31
32
|
strict: boolean;
|
|
@@ -76,6 +77,11 @@ export interface RunCheckJsGroupedInput {
|
|
|
76
77
|
* targets.
|
|
77
78
|
*/
|
|
78
79
|
rootScope?: ReadonlySet<string>;
|
|
80
|
+
/**
|
|
81
|
+
* Parsed source files shared across several programs built with the same
|
|
82
|
+
* compiler options and shim in one run. See {@link runCheckJsTestFilesGrouped}.
|
|
83
|
+
*/
|
|
84
|
+
sourceFileCache?: CheckJsSourceFileCache;
|
|
79
85
|
}
|
|
80
86
|
/**
|
|
81
87
|
* Builds the checkJs program once over `resolutionOwned` and returns its
|
|
@@ -30,6 +30,7 @@ import { toError } from '../utils/errors.js';
|
|
|
30
30
|
import { pathExists } from '../utils/fs.js';
|
|
31
31
|
import { verbose } from '../utils/logger.js';
|
|
32
32
|
import { normalizePathSlashes } from '../utils/paths.js';
|
|
33
|
+
import { withSharedSourceFiles } from './patch-lint-checkjs-cache.js';
|
|
33
34
|
import { collectUnmanagedCompanions, retargetUnmanagedCompanionHints, } from './patch-lint-unmanaged-companion.js';
|
|
34
35
|
import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, TEST_HARNESS_SHIM, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
|
|
35
36
|
// ---------------------------------------------------------------------------
|
|
@@ -340,7 +341,7 @@ export async function runCheckJsGrouped(input) {
|
|
|
340
341
|
});
|
|
341
342
|
},
|
|
342
343
|
};
|
|
343
|
-
const program = ts.createProgram(rootFiles, options, host);
|
|
344
|
+
const program = ts.createProgram(rootFiles, options, withSharedSourceFiles(host, input.sourceFileCache, shimPath, shimSource));
|
|
344
345
|
const byFile = groupOwnedDiagnostics(ts, [...program.getSemanticDiagnostics(), ...program.getSyntacticDiagnostics()], relByAbsolute, input.mode?.undefinedIdentifiers ?? 'warning');
|
|
345
346
|
verbose(`checkJs: analyzed ${rootFiles.length - 1} file(s) across ${byFile.size} owning file(s)`);
|
|
346
347
|
return { byFile, global: [] };
|
|
@@ -461,6 +462,11 @@ export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint,
|
|
|
461
462
|
const mode = modeFromPatchLintConfig(patchLint);
|
|
462
463
|
const seenGlobal = new Set();
|
|
463
464
|
const files = [...testFiles].sort((a, b) => a.localeCompare(b));
|
|
465
|
+
// Every test program below shares options and shim, and mostly the same
|
|
466
|
+
// files: TypeScript's lib declarations, the shim, and the directory's
|
|
467
|
+
// head.js helpers. Parsing them once instead of once per test file took
|
|
468
|
+
// a 467-test queue's pass from 66 s to a fraction of that.
|
|
469
|
+
const sourceFileCache = new Map();
|
|
464
470
|
const companions = await collectUnmanagedCompanions(repoDir, files, testFiles);
|
|
465
471
|
for (const file of files) {
|
|
466
472
|
// Under a rootScope only the scoped files get their own program, but
|
|
@@ -487,6 +493,7 @@ export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint,
|
|
|
487
493
|
projectRoot,
|
|
488
494
|
mode,
|
|
489
495
|
builtinShimSuffix: TEST_HARNESS_SHIM,
|
|
496
|
+
sourceFileCache,
|
|
490
497
|
});
|
|
491
498
|
const own = result.byFile.get(file);
|
|
492
499
|
if (own && own.length > 0) {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { PatchLintIssue, PatchMetadata } from '../types/commands/index.js';
|
|
13
13
|
import type { FireForgeConfig, PatchPolicyConfig } from '../types/config.js';
|
|
14
|
+
import type { ForwardRegistrationScope } from './patch-lint-forward-registration-extended.js';
|
|
14
15
|
/**
|
|
15
16
|
* One patch's contribution to {@link PatchQueueContext}.
|
|
16
17
|
*
|
|
@@ -39,7 +40,7 @@ export interface PatchQueueEntry {
|
|
|
39
40
|
* reconstructable `GIT binary patch`, or a payload-less
|
|
40
41
|
* `Binary files … differ` stub. `newFiles` is text-only by design (a
|
|
41
42
|
* binary authors no imports, so the import rules gain nothing from it and
|
|
42
|
-
* `
|
|
43
|
+
* `extractNewFileContentFromDiff` correctly refuses to decode one), but "does a
|
|
43
44
|
* later patch create this path" is a question about existence after apply,
|
|
44
45
|
* not about content, and answering it from `newFiles` made every binary
|
|
45
46
|
* creation invisible to the staged-registration rules.
|
|
@@ -76,6 +77,11 @@ export interface PatchQueueContext {
|
|
|
76
77
|
* Absent = current behavior (hint always printed).
|
|
77
78
|
*/
|
|
78
79
|
patchPolicy?: PatchPolicyConfig;
|
|
80
|
+
/**
|
|
81
|
+
* Which carriers the forward-registration rule checks, from
|
|
82
|
+
* `patchLint.forwardRegistration`. Absent = `support-files` only.
|
|
83
|
+
*/
|
|
84
|
+
forwardRegistration?: ForwardRegistrationScope;
|
|
79
85
|
}
|
|
80
86
|
/**
|
|
81
87
|
* Builds a {@link PatchQueueContext} by reading every .patch file in the
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
* module.
|
|
12
12
|
*/
|
|
13
13
|
import { basename } from 'node:path';
|
|
14
|
-
import { toError } from '../utils/errors.js';
|
|
15
14
|
import { readText } from '../utils/fs.js';
|
|
16
15
|
import { verbose } from '../utils/logger.js';
|
|
17
16
|
import { stripJsComments } from '../utils/regex.js';
|
|
@@ -24,7 +23,7 @@ import { lintPatchQueueModuleRegistrations } from './patch-lint-module-registrat
|
|
|
24
23
|
import { describeRegistrationFailure, findStagedRegistrationFailure, isLaterOwner, quoteRegistrationLine, } from './patch-lint-staged-registration.js';
|
|
25
24
|
import { loadPatchesManifest } from './patch-manifest-io.js';
|
|
26
25
|
import { categoryRangeForOrder, categoryRangeLabel } from './patch-policy.js';
|
|
27
|
-
import {
|
|
26
|
+
import { extractNewFileContentsFromDiff } from './patch-transform.js';
|
|
28
27
|
/**
|
|
29
28
|
* Builds a {@link PatchQueueContext} by reading every .patch file in the
|
|
30
29
|
* directory and extracting new-file content for each creation.
|
|
@@ -50,15 +49,11 @@ export async function buildPatchQueueContext(patchesDir, config) {
|
|
|
50
49
|
for (const patch of patches) {
|
|
51
50
|
const diff = await readText(patch.path);
|
|
52
51
|
const newFilePaths = detectNewFilesInDiff(diff);
|
|
53
|
-
|
|
54
|
-
for
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
verbose(`Skipping forward-import scan for ${newFile} in ${patch.filename}: ${toError(error).message}`);
|
|
61
|
-
}
|
|
52
|
+
// One parse of the body already in hand. Extracting per file re-read
|
|
53
|
+
// the whole patch from disk for every file it creates.
|
|
54
|
+
const { contents: newFiles, refused } = extractNewFileContentsFromDiff(diff, newFilePaths);
|
|
55
|
+
for (const newFile of refused) {
|
|
56
|
+
verbose(`Skipping forward-import scan for ${newFile} in ${patch.filename}: binary section cannot be extracted as text`);
|
|
62
57
|
}
|
|
63
58
|
// Added-line content for every file the patch modifies but does not
|
|
64
59
|
// create. Fed to the forward-import rule so imports introduced into
|
|
@@ -87,7 +82,13 @@ export async function buildPatchQueueContext(patchesDir, config) {
|
|
|
87
82
|
}
|
|
88
83
|
// Sort by order so rules can rely on entries being in apply order.
|
|
89
84
|
entries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
90
|
-
return {
|
|
85
|
+
return {
|
|
86
|
+
entries,
|
|
87
|
+
...(config?.patchPolicy ? { patchPolicy: config.patchPolicy } : {}),
|
|
88
|
+
...(config?.patchLint?.forwardRegistration !== undefined
|
|
89
|
+
? { forwardRegistration: config.patchLint.forwardRegistration }
|
|
90
|
+
: {}),
|
|
91
|
+
};
|
|
91
92
|
}
|
|
92
93
|
// The creators map (split out of `lintPatchQueueDuplicateCreations` so
|
|
93
94
|
// `status --ownership` can consume the structured data) lives in
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PatchQueueForwardRegistrationEntry, PatchQueueView } from './patch-lint-queue-types.js';
|
|
2
|
+
/** Which carriers `forward-registration` checks. */
|
|
3
|
+
export type ForwardRegistrationScope = 'support-files' | 'extended';
|
|
4
|
+
/** The carrier kind a forward registration was found in. */
|
|
5
|
+
export type ForwardRegistrationKind = 'support-files' | 'jar-mn' | 'moz-build' | 'test-manifest' | 'custom-elements';
|
|
6
|
+
/** One undeclared forward registration: a line naming a file a later patch creates. */
|
|
7
|
+
export interface ForwardRegistration {
|
|
8
|
+
/** The patch that adds the registration line. */
|
|
9
|
+
patch: string;
|
|
10
|
+
/** Carrier kind. */
|
|
11
|
+
kind: ForwardRegistrationKind;
|
|
12
|
+
/** Engine-relative path of the carrier file (the manifest, jar.mn, …). */
|
|
13
|
+
file: string;
|
|
14
|
+
/** The added line, whitespace-trimmed, that names the target. */
|
|
15
|
+
line: string;
|
|
16
|
+
/** The token on that line that names the target, as written. */
|
|
17
|
+
token: string;
|
|
18
|
+
/** Engine-relative path of the file the later patch creates. */
|
|
19
|
+
creates: string;
|
|
20
|
+
/** The later patch that creates it. */
|
|
21
|
+
owner: string;
|
|
22
|
+
/** How many distinct added lines in the carrier name the same target. */
|
|
23
|
+
occurrences: number;
|
|
24
|
+
/** Paste-and-run declaration that records the stage as intentional. */
|
|
25
|
+
command: string;
|
|
26
|
+
}
|
|
27
|
+
/** The discharge command, in the exact shape the support-files arm prints. */
|
|
28
|
+
export declare function formatRegistrationDeclaration(patch: string, file: string, line: string, creates: string, owner: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Finds every undeclared forward registration in the extended carriers:
|
|
31
|
+
* a registration line a patch adds that names a file only a later-ordered
|
|
32
|
+
* patch creates. Creators come from `createdFiles` (the `new file mode`
|
|
33
|
+
* sections), never `filesAffected`.
|
|
34
|
+
*
|
|
35
|
+
* @param ctx - Queue in application order
|
|
36
|
+
* @returns One record per (patch, carrier, created file), first line quoted
|
|
37
|
+
*/
|
|
38
|
+
export declare function findExtendedForwardRegistrations(ctx: PatchQueueView<PatchQueueForwardRegistrationEntry>): ForwardRegistration[];
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* The opt-in arms of the `forward-registration` rule
|
|
4
|
+
* (`patchLint.forwardRegistration: "extended"`).
|
|
5
|
+
*
|
|
6
|
+
* The default rule covers test-manifest `support-files` only. These arms
|
|
7
|
+
* cover the carriers whose target resolves WITHOUT the build system:
|
|
8
|
+
*
|
|
9
|
+
* - a jar.mn pair line: the packaged source is on the line, in parentheses,
|
|
10
|
+
* relative to the jar.mn directory (or `/`-rooted at the source root);
|
|
11
|
+
* - a moz.build path token (a quoted string with a `.` or `/`), relative to
|
|
12
|
+
* the moz.build directory (or `/`-rooted);
|
|
13
|
+
* - a test manifest's section header (`["browser_foo.js"]`) and `head`,
|
|
14
|
+
* relative to the manifest directory;
|
|
15
|
+
* - a `customElements.js` `chrome://<pkg>/content/<path>` URL, resolved
|
|
16
|
+
* through the jar.mn lines the same queue adds (`content/<pkg>/<path>`
|
|
17
|
+
* is the jar target that URL serves).
|
|
18
|
+
*
|
|
19
|
+
* Anything else stays silent: a preprocessor or locale substitution, a
|
|
20
|
+
* `**` or `!` pattern, a path that climbs out of the tree, a chrome URL no
|
|
21
|
+
* queue jar.mn line packages. A rule that guessed there would refuse
|
|
22
|
+
* correct queues on a Firefox-sized tree.
|
|
23
|
+
*
|
|
24
|
+
* Opt-in because a queue that predates it can carry many such edges on a
|
|
25
|
+
* ratchet, and a new error on install would red a `--max-warnings 0` gate.
|
|
26
|
+
*/
|
|
27
|
+
import { basename, dirname, posix } from 'node:path';
|
|
28
|
+
import { normalizePathSlashes } from '../utils/paths.js';
|
|
29
|
+
import { escapeRegex } from '../utils/regex.js';
|
|
30
|
+
import { parseJarMnEntry } from './build-audit-registration.js';
|
|
31
|
+
import { isLaterOwner, quoteRegistrationLine } from './patch-lint-staged-registration.js';
|
|
32
|
+
const TEST_MANIFEST_BASENAMES = new Set([
|
|
33
|
+
'xpcshell.toml',
|
|
34
|
+
'xpcshell.ini',
|
|
35
|
+
'mochitest.toml',
|
|
36
|
+
'mochitest.ini',
|
|
37
|
+
'browser.toml',
|
|
38
|
+
'browser.ini',
|
|
39
|
+
'chrome.toml',
|
|
40
|
+
'chrome.ini',
|
|
41
|
+
'a11y.toml',
|
|
42
|
+
'a11y.ini',
|
|
43
|
+
]);
|
|
44
|
+
/**
|
|
45
|
+
* Resolves a path token against a base directory into a matcher. A leading
|
|
46
|
+
* `/` is rooted at the source tree. Only a `*` inside one segment is
|
|
47
|
+
* expanded. Substitutions (`%`, `@`), `**`, `!` and paths that climb out of
|
|
48
|
+
* the tree resolve to nothing.
|
|
49
|
+
*/
|
|
50
|
+
function resolvePathToken(baseDir, token) {
|
|
51
|
+
if (/[%@!\s]/.test(token) || token.includes('**'))
|
|
52
|
+
return undefined;
|
|
53
|
+
const joined = token.startsWith('/')
|
|
54
|
+
? posix.normalize(token.slice(1))
|
|
55
|
+
: posix.normalize(posix.join(baseDir, normalizePathSlashes(token)));
|
|
56
|
+
if (joined.startsWith('..') || joined === '.' || joined.length === 0)
|
|
57
|
+
return undefined;
|
|
58
|
+
if (!joined.includes('*')) {
|
|
59
|
+
return { exact: joined, matches: (candidate) => candidate === joined };
|
|
60
|
+
}
|
|
61
|
+
const pattern = new RegExp(`^${joined
|
|
62
|
+
.split('*')
|
|
63
|
+
.map((part) => escapeRegex(part))
|
|
64
|
+
.join('[^/]*')}$`);
|
|
65
|
+
return { matches: (candidate) => pattern.test(candidate) };
|
|
66
|
+
}
|
|
67
|
+
function carrierDir(file) {
|
|
68
|
+
const dir = normalizePathSlashes(dirname(file));
|
|
69
|
+
return dir === '.' ? '' : dir;
|
|
70
|
+
}
|
|
71
|
+
function jarMnReferences(file, content) {
|
|
72
|
+
const refs = [];
|
|
73
|
+
for (const raw of content.split('\n')) {
|
|
74
|
+
const parsed = parseJarMnEntry(raw);
|
|
75
|
+
if (!parsed)
|
|
76
|
+
continue;
|
|
77
|
+
const resolved = resolvePathToken(carrierDir(file), parsed.source);
|
|
78
|
+
if (resolved)
|
|
79
|
+
refs.push({ kind: 'jar-mn', file, line: raw.trim(), token: parsed.source, ...resolved });
|
|
80
|
+
}
|
|
81
|
+
return refs;
|
|
82
|
+
}
|
|
83
|
+
function mozBuildReferences(file, content) {
|
|
84
|
+
const refs = [];
|
|
85
|
+
for (const raw of content.split('\n')) {
|
|
86
|
+
if (raw.trimStart().startsWith('#'))
|
|
87
|
+
continue;
|
|
88
|
+
for (const quoted of raw.matchAll(/["']([^"']+)["']/g)) {
|
|
89
|
+
const token = quoted[1] ?? '';
|
|
90
|
+
// A path names a file: it has an extension or a directory. Bare words
|
|
91
|
+
// ("Firefox", a define's value) are not paths.
|
|
92
|
+
if (!token.includes('.') && !token.includes('/'))
|
|
93
|
+
continue;
|
|
94
|
+
const resolved = resolvePathToken(carrierDir(file), token);
|
|
95
|
+
if (resolved)
|
|
96
|
+
refs.push({ kind: 'moz-build', file, line: raw.trim(), token, ...resolved });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return refs;
|
|
100
|
+
}
|
|
101
|
+
function testManifestReferences(file, content) {
|
|
102
|
+
const refs = [];
|
|
103
|
+
for (const raw of content.split('\n')) {
|
|
104
|
+
const section = /^\s*\[\s*["']?([^"'\]]+?)["']?\s*\]\s*$/.exec(raw);
|
|
105
|
+
const head = /^\s*head\s*=\s*["']?([^"'\s]+)["']?\s*$/.exec(raw);
|
|
106
|
+
const token = section?.[1] ?? head?.[1];
|
|
107
|
+
if (token === undefined || token === 'DEFAULT')
|
|
108
|
+
continue;
|
|
109
|
+
const resolved = resolvePathToken(carrierDir(file), token);
|
|
110
|
+
if (resolved)
|
|
111
|
+
refs.push({ kind: 'test-manifest', file, line: raw.trim(), token, ...resolved });
|
|
112
|
+
}
|
|
113
|
+
return refs;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* jar target → packaged source, over every jar.mn line the queue adds. Only
|
|
117
|
+
* exact (wildcard-free) sources can back a chrome URL lookup.
|
|
118
|
+
*/
|
|
119
|
+
function buildJarTargetIndex(entries) {
|
|
120
|
+
const index = new Map();
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
for (const [file, content] of [...entry.newFiles, ...entry.modifiedFileAdditions]) {
|
|
123
|
+
if (basename(normalizePathSlashes(file)) !== 'jar.mn')
|
|
124
|
+
continue;
|
|
125
|
+
for (const raw of content.split('\n')) {
|
|
126
|
+
const parsed = parseJarMnEntry(raw);
|
|
127
|
+
if (!parsed)
|
|
128
|
+
continue;
|
|
129
|
+
const resolved = resolvePathToken(carrierDir(file), parsed.source);
|
|
130
|
+
if (resolved?.exact !== undefined)
|
|
131
|
+
index.set(parsed.target, resolved.exact);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return index;
|
|
136
|
+
}
|
|
137
|
+
function customElementsReferences(file, content, jarTargets) {
|
|
138
|
+
const refs = [];
|
|
139
|
+
for (const raw of content.split('\n')) {
|
|
140
|
+
for (const url of raw.matchAll(/chrome:\/\/([\w.-]+)\/content\/([^"'\s)]+)/g)) {
|
|
141
|
+
const source = jarTargets.get(`content/${url[1] ?? ''}/${url[2] ?? ''}`);
|
|
142
|
+
if (source === undefined)
|
|
143
|
+
continue;
|
|
144
|
+
refs.push({
|
|
145
|
+
kind: 'custom-elements',
|
|
146
|
+
file,
|
|
147
|
+
line: raw.trim(),
|
|
148
|
+
token: url[0],
|
|
149
|
+
exact: source,
|
|
150
|
+
matches: (candidate) => candidate === source,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return refs;
|
|
155
|
+
}
|
|
156
|
+
function referencesFor(entry, jarTargets) {
|
|
157
|
+
const refs = [];
|
|
158
|
+
for (const [rawPath, content] of [...entry.newFiles, ...entry.modifiedFileAdditions]) {
|
|
159
|
+
const file = normalizePathSlashes(rawPath);
|
|
160
|
+
const name = basename(file);
|
|
161
|
+
if (name === 'jar.mn')
|
|
162
|
+
refs.push(...jarMnReferences(file, content));
|
|
163
|
+
else if (name === 'moz.build')
|
|
164
|
+
refs.push(...mozBuildReferences(file, content));
|
|
165
|
+
else if (TEST_MANIFEST_BASENAMES.has(name))
|
|
166
|
+
refs.push(...testManifestReferences(file, content));
|
|
167
|
+
else if (name === 'customElements.js') {
|
|
168
|
+
refs.push(...customElementsReferences(file, content, jarTargets));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return refs;
|
|
172
|
+
}
|
|
173
|
+
function isDeclared(entry, file, created) {
|
|
174
|
+
return (entry.metadata?.stagedDependencies?.registrations ?? []).some((registration) => normalizePathSlashes(registration.file) === file &&
|
|
175
|
+
normalizePathSlashes(registration.creates) === created);
|
|
176
|
+
}
|
|
177
|
+
/** The discharge command, in the exact shape the support-files arm prints. */
|
|
178
|
+
export function formatRegistrationDeclaration(patch, file, line, creates, owner) {
|
|
179
|
+
return (`fireforge patch staged-dependency ${patch} --add --kind registration ` +
|
|
180
|
+
`--file ${file} --line "${quoteRegistrationLine(line)}" ` +
|
|
181
|
+
`--creates ${creates} --owner ${owner}`);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Finds every undeclared forward registration in the extended carriers:
|
|
185
|
+
* a registration line a patch adds that names a file only a later-ordered
|
|
186
|
+
* patch creates. Creators come from `createdFiles` (the `new file mode`
|
|
187
|
+
* sections), never `filesAffected`.
|
|
188
|
+
*
|
|
189
|
+
* @param ctx - Queue in application order
|
|
190
|
+
* @returns One record per (patch, carrier, created file), first line quoted
|
|
191
|
+
*/
|
|
192
|
+
export function findExtendedForwardRegistrations(ctx) {
|
|
193
|
+
const jarTargets = buildJarTargetIndex(ctx.entries);
|
|
194
|
+
const creatorsByPath = new Map();
|
|
195
|
+
for (const entry of ctx.entries) {
|
|
196
|
+
for (const created of entry.createdFiles) {
|
|
197
|
+
const path = normalizePathSlashes(created);
|
|
198
|
+
const list = creatorsByPath.get(path) ?? [];
|
|
199
|
+
list.push(entry);
|
|
200
|
+
creatorsByPath.set(path, list);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const found = new Map();
|
|
204
|
+
const linesByKey = new Map();
|
|
205
|
+
for (const entry of ctx.entries) {
|
|
206
|
+
for (const ref of referencesFor(entry, jarTargets)) {
|
|
207
|
+
const candidates = ref.exact !== undefined
|
|
208
|
+
? (creatorsByPath.get(ref.exact) ?? []).map((owner) => [ref.exact ?? '', owner])
|
|
209
|
+
: [...creatorsByPath].flatMap(([path, owners]) => ref.matches(path) ? owners.map((owner) => [path, owner]) : []);
|
|
210
|
+
for (const [created, later] of candidates) {
|
|
211
|
+
if (!isLaterOwner(later, entry))
|
|
212
|
+
continue;
|
|
213
|
+
if (isDeclared(entry, ref.file, created))
|
|
214
|
+
continue;
|
|
215
|
+
const key = `${entry.filename}|${ref.file}|${created}|${later.filename}`;
|
|
216
|
+
const existing = found.get(key);
|
|
217
|
+
if (existing !== undefined) {
|
|
218
|
+
const lines = linesByKey.get(key) ?? new Set();
|
|
219
|
+
lines.add(ref.line);
|
|
220
|
+
linesByKey.set(key, lines);
|
|
221
|
+
existing.occurrences = lines.size;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
linesByKey.set(key, new Set([ref.line]));
|
|
225
|
+
found.set(key, {
|
|
226
|
+
patch: entry.filename,
|
|
227
|
+
kind: ref.kind,
|
|
228
|
+
file: ref.file,
|
|
229
|
+
line: ref.line,
|
|
230
|
+
token: ref.token,
|
|
231
|
+
creates: created,
|
|
232
|
+
owner: later.filename,
|
|
233
|
+
occurrences: 1,
|
|
234
|
+
command: formatRegistrationDeclaration(entry.filename, ref.file, ref.line, created, later.filename),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return [...found.values()];
|
|
240
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
2
|
+
import { type ForwardRegistration, type ForwardRegistrationScope } from './patch-lint-forward-registration-extended.js';
|
|
2
3
|
import type { PatchQueueForwardRegistrationEntry, PatchQueueView } from './patch-lint-queue-types.js';
|
|
3
4
|
/**
|
|
4
5
|
* One `support-files` entry, with the line the patch actually adds it on.
|
|
@@ -48,11 +49,23 @@ export declare function extractSupportFileEntries(content: string): SupportFileE
|
|
|
48
49
|
*/
|
|
49
50
|
export declare function buildSupportFileMatcher(manifestPath: string, entry: string): ((candidate: string) => boolean) | undefined;
|
|
50
51
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* Every undeclared forward registration in the queue, over the carriers
|
|
53
|
+
* `scope` selects: `support-files` (the default) or `extended` (adds
|
|
54
|
+
* jar.mn, moz.build, test-manifest sections and customElements.js; see
|
|
55
|
+
* `patch-lint-forward-registration-extended.ts`).
|
|
54
56
|
*
|
|
55
|
-
* @param ctx -
|
|
57
|
+
* @param ctx - Queue in application order
|
|
58
|
+
* @param scope - Which carriers to check
|
|
59
|
+
*/
|
|
60
|
+
export declare function collectForwardRegistrations(ctx: PatchQueueView<PatchQueueForwardRegistrationEntry>, scope?: ForwardRegistrationScope): ForwardRegistration[];
|
|
61
|
+
/**
|
|
62
|
+
* Flags every forward registration the queue's scope selects, unless the
|
|
63
|
+
* staged dependency is declared.
|
|
64
|
+
*
|
|
65
|
+
* @param ctx - Projected queue in application order, with its
|
|
66
|
+
* `forwardRegistration` scope (default `support-files`)
|
|
56
67
|
* @returns One issue per undeclared forward registration
|
|
57
68
|
*/
|
|
58
|
-
export declare function lintPatchQueueForwardRegistrations(ctx: PatchQueueView<PatchQueueForwardRegistrationEntry>
|
|
69
|
+
export declare function lintPatchQueueForwardRegistrations(ctx: PatchQueueView<PatchQueueForwardRegistrationEntry> & {
|
|
70
|
+
forwardRegistration?: ForwardRegistrationScope;
|
|
71
|
+
}): PatchLintIssue[];
|