@hominis/fireforge 0.47.1 → 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 +54 -0
- package/dist/bin/fireforge.js +5 -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/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 +1 -0
- package/dist/src/core/config-validate.js +7 -0
- package/dist/src/core/discard-baseline.js +2 -1
- 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/mach-mozconfig.d.ts +2 -1
- package/dist/src/core/mach-mozconfig.js +2 -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 +15 -0
- package/dist/src/types/config.d.ts +10 -0
- 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 +2 -0
- package/dist/src/utils/process.js +2 -1
- package/dist/src/utils/validation.d.ts +11 -0
- package/dist/src/utils/validation.js +43 -0
- package/docs/configuration.md +81 -0
- package/docs/exit-codes.md +1 -1
- package/docs/lifecycle-invariants.md +4 -3
- package/docs/patch-workflow.md +35 -1
- package/package.json +7 -3
|
@@ -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[];
|
|
@@ -11,17 +11,20 @@
|
|
|
11
11
|
* removing the declaration produced identical silence, so the gap was
|
|
12
12
|
* invisible rather than merely absent.
|
|
13
13
|
*
|
|
14
|
-
* Scope is narrow
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
14
|
+
* Scope is narrow by default: test-manifest `support-files` only. The
|
|
15
|
+
* carriers whose target resolves without the build system (jar.mn pair
|
|
16
|
+
* lines, moz.build path tokens, test-manifest sections, customElements.js
|
|
17
|
+
* chrome URLs) are opt-in through `patchLint.forwardRegistration:
|
|
18
|
+
* "extended"` (see `patch-lint-forward-registration-extended.ts`), since a
|
|
19
|
+
* queue that predates them can carry such edges on a ratchet and a new
|
|
20
|
+
* error on install would red its gate. The staged-registration declaration
|
|
21
|
+
* remains the documented escape hatch for a deliberate stage.
|
|
20
22
|
*/
|
|
21
23
|
import { basename, dirname, posix } from 'node:path';
|
|
22
24
|
import { normalizePathSlashes } from '../utils/paths.js';
|
|
23
25
|
import { escapeRegex } from '../utils/regex.js';
|
|
24
|
-
import {
|
|
26
|
+
import { findExtendedForwardRegistrations, formatRegistrationDeclaration, } from './patch-lint-forward-registration-extended.js';
|
|
27
|
+
import { isLaterOwner } from './patch-lint-staged-registration.js';
|
|
25
28
|
/**
|
|
26
29
|
* Manifest basenames whose `support-files` key registers auxiliary files
|
|
27
30
|
* for a test suite. Closed and documented on purpose: a pattern like
|
|
@@ -142,15 +145,15 @@ function isDeclared(entry, manifestPath, createdPath) {
|
|
|
142
145
|
normalizePathSlashes(registration.creates) === normalizePathSlashes(createdPath));
|
|
143
146
|
}
|
|
144
147
|
/**
|
|
145
|
-
*
|
|
148
|
+
* Finds every `support-files` registration a patch introduces that names a
|
|
146
149
|
* file only a later-ordered patch creates, unless the staged dependency is
|
|
147
150
|
* declared.
|
|
148
151
|
*
|
|
149
152
|
* @param ctx - Projected queue in application order
|
|
150
|
-
* @returns One
|
|
153
|
+
* @returns One record per (patch, manifest, entry, created file)
|
|
151
154
|
*/
|
|
152
|
-
|
|
153
|
-
const
|
|
155
|
+
function findSupportFileForwardRegistrations(ctx) {
|
|
156
|
+
const found = [];
|
|
154
157
|
for (const entry of ctx.entries) {
|
|
155
158
|
// Both arms: a manifest this patch creates and one it modifies can each
|
|
156
159
|
// introduce a registration line.
|
|
@@ -163,17 +166,6 @@ export function lintPatchQueueForwardRegistrations(ctx) {
|
|
|
163
166
|
const matches = buildSupportFileMatcher(manifestPath, raw);
|
|
164
167
|
if (matches === undefined)
|
|
165
168
|
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
169
|
for (const later of ctx.entries) {
|
|
178
170
|
// Same predicate the declared-registration arm validates against,
|
|
179
171
|
// tiebreak included: two spellings of "later" would let one rule
|
|
@@ -185,26 +177,80 @@ export function lintPatchQueueForwardRegistrations(ctx) {
|
|
|
185
177
|
continue;
|
|
186
178
|
if (isDeclared(entry, manifestPath, created))
|
|
187
179
|
continue;
|
|
188
|
-
|
|
180
|
+
found.push({
|
|
181
|
+
patch: entry.filename,
|
|
182
|
+
kind: 'support-files',
|
|
189
183
|
file: manifestPath,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
severity: 'error',
|
|
184
|
+
// `--line` is the line as the patch adds it, which is what the
|
|
185
|
+
// declared arm compares and what `--remove` matches on.
|
|
186
|
+
// Synthesising `support-files = ["<entry>"]` instead only ever
|
|
187
|
+
// matched a single-line single-entry array, so the pasted
|
|
188
|
+
// command left the declaring patch red with
|
|
189
|
+
// staged-dependency-unused.
|
|
190
|
+
line: addedLine,
|
|
191
|
+
token: raw,
|
|
192
|
+
creates: created,
|
|
193
|
+
owner: later.filename,
|
|
194
|
+
occurrences,
|
|
195
|
+
command: formatRegistrationDeclaration(entry.filename, manifestPath, addedLine, created, later.filename),
|
|
203
196
|
});
|
|
204
197
|
}
|
|
205
198
|
}
|
|
206
199
|
}
|
|
207
200
|
}
|
|
208
201
|
}
|
|
209
|
-
return
|
|
202
|
+
return found;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Every undeclared forward registration in the queue, over the carriers
|
|
206
|
+
* `scope` selects: `support-files` (the default) or `extended` (adds
|
|
207
|
+
* jar.mn, moz.build, test-manifest sections and customElements.js; see
|
|
208
|
+
* `patch-lint-forward-registration-extended.ts`).
|
|
209
|
+
*
|
|
210
|
+
* @param ctx - Queue in application order
|
|
211
|
+
* @param scope - Which carriers to check
|
|
212
|
+
*/
|
|
213
|
+
export function collectForwardRegistrations(ctx, scope = 'support-files') {
|
|
214
|
+
const found = findSupportFileForwardRegistrations(ctx);
|
|
215
|
+
if (scope === 'extended')
|
|
216
|
+
found.push(...findExtendedForwardRegistrations(ctx));
|
|
217
|
+
return found;
|
|
218
|
+
}
|
|
219
|
+
const KIND_PHRASES = {
|
|
220
|
+
'support-files': 'as a support file',
|
|
221
|
+
'jar-mn': 'for packaging',
|
|
222
|
+
'moz-build': 'in the build',
|
|
223
|
+
'test-manifest': 'as a test',
|
|
224
|
+
'custom-elements': 'as a custom element',
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Flags every forward registration the queue's scope selects, unless the
|
|
228
|
+
* staged dependency is declared.
|
|
229
|
+
*
|
|
230
|
+
* @param ctx - Projected queue in application order, with its
|
|
231
|
+
* `forwardRegistration` scope (default `support-files`)
|
|
232
|
+
* @returns One issue per undeclared forward registration
|
|
233
|
+
*/
|
|
234
|
+
export function lintPatchQueueForwardRegistrations(ctx) {
|
|
235
|
+
return collectForwardRegistrations(ctx, ctx.forwardRegistration).map((found) => {
|
|
236
|
+
const ambiguity = found.occurrences > 1
|
|
237
|
+
? ` (the entry is spelled out on ${found.occurrences} added lines here; ` +
|
|
238
|
+
'the first is quoted, and one declaration covers the file either way ' +
|
|
239
|
+
'because the declared check keys on --file and --creates.)'
|
|
240
|
+
: '';
|
|
241
|
+
return {
|
|
242
|
+
file: found.file,
|
|
243
|
+
check: 'forward-registration',
|
|
244
|
+
patches: [found.patch],
|
|
245
|
+
fingerprint: `forward-registration|${found.patch}|${found.file}|${found.token}|${found.creates}|${found.owner}`,
|
|
246
|
+
message: `${found.file} in ${found.patch} registers "${found.token}" ${KIND_PHRASES[found.kind]}, ` +
|
|
247
|
+
`but ${found.creates} is created by the later patch ${found.owner}. ` +
|
|
248
|
+
'Applying the queue up to this patch leaves the registration dangling. ' +
|
|
249
|
+
'Reorder the patches so the file is created first, move the registration into ' +
|
|
250
|
+
'the later patch, or declare the intentional staged dependency with: ' +
|
|
251
|
+
found.command +
|
|
252
|
+
ambiguity,
|
|
253
|
+
severity: 'error',
|
|
254
|
+
};
|
|
255
|
+
});
|
|
210
256
|
}
|
|
@@ -25,6 +25,17 @@ export declare function countNonBinaryDiffLines(diffContent: string): {
|
|
|
25
25
|
total: number;
|
|
26
26
|
textLines: number;
|
|
27
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* The files the `file-too-large` rule measures for a patch: the JS files
|
|
30
|
+
* (`.js`, `.mjs`, `.sys.mjs`, …) the patch creates. A modified file is
|
|
31
|
+
* never measured, and neither is a new file of another kind (CSS, FTL,
|
|
32
|
+
* markup), however long. This is the rule's own predicate, not a mirror
|
|
33
|
+
* of it: the rule selects its files through the same function.
|
|
34
|
+
*
|
|
35
|
+
* @param patchText - Unified-diff body of one patch
|
|
36
|
+
* @returns Engine-relative paths the rule would size-check
|
|
37
|
+
*/
|
|
38
|
+
export declare function filesMeasuredByFileTooLarge(patchText: string): Set<string>;
|
|
28
39
|
/**
|
|
29
40
|
* Returns true if the file path looks like a test file.
|
|
30
41
|
* Matches paths containing `/test/` or filenames starting with
|
|
@@ -189,6 +189,23 @@ function isBrandingOnlyPatch(files) {
|
|
|
189
189
|
function isJsFile(file) {
|
|
190
190
|
return JS_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
191
191
|
}
|
|
192
|
+
/** The subset of a patch's new files that `file-too-large` measures. */
|
|
193
|
+
function measuredByFileTooLarge(newFiles) {
|
|
194
|
+
return new Set([...newFiles].filter(isJsFile));
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The files the `file-too-large` rule measures for a patch: the JS files
|
|
198
|
+
* (`.js`, `.mjs`, `.sys.mjs`, …) the patch creates. A modified file is
|
|
199
|
+
* never measured, and neither is a new file of another kind (CSS, FTL,
|
|
200
|
+
* markup), however long. This is the rule's own predicate, not a mirror
|
|
201
|
+
* of it: the rule selects its files through the same function.
|
|
202
|
+
*
|
|
203
|
+
* @param patchText - Unified-diff body of one patch
|
|
204
|
+
* @returns Engine-relative paths the rule would size-check
|
|
205
|
+
*/
|
|
206
|
+
export function filesMeasuredByFileTooLarge(patchText) {
|
|
207
|
+
return measuredByFileTooLarge(detectNewFilesInDiff(patchText));
|
|
208
|
+
}
|
|
192
209
|
/**
|
|
193
210
|
* Returns true if the file path looks like a test file.
|
|
194
211
|
* Matches paths containing `/test/` or filenames starting with
|
|
@@ -387,6 +404,7 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
|
|
|
387
404
|
return [];
|
|
388
405
|
const issues = [];
|
|
389
406
|
const binaryName = config.binaryName.toLowerCase();
|
|
407
|
+
const sizeMeasured = measuredByFileTooLarge(newFiles);
|
|
390
408
|
for (const file of jsFiles) {
|
|
391
409
|
const filePath = join(repoDir, file);
|
|
392
410
|
if (!(await pathExists(filePath)))
|
|
@@ -404,8 +422,8 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
|
|
|
404
422
|
severity: 'error',
|
|
405
423
|
});
|
|
406
424
|
}
|
|
407
|
-
// 2. File size check (new files only)
|
|
408
|
-
if (
|
|
425
|
+
// 2. File size check (new JS files only; see filesMeasuredByFileTooLarge)
|
|
426
|
+
if (sizeMeasured.has(file)) {
|
|
409
427
|
const lineCount = countContentLines(content);
|
|
410
428
|
const isTest = isTestFile(file);
|
|
411
429
|
const thresholds = resolveFileSizeThresholds(config.patchLint?.fileSizeThresholds, isTest);
|
|
@@ -4,10 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
6
|
* Extracts the complete file content from a "new file" patch given a raw
|
|
7
|
-
* diff string already in memory.
|
|
8
|
-
*
|
|
9
|
-
* that already hold the diff (e.g. the in-flight export planner) and do
|
|
10
|
-
* not want to round-trip through the filesystem.
|
|
7
|
+
* diff string already in memory. A caller with only a patch file path
|
|
8
|
+
* reads it first; every caller holds the diff already.
|
|
11
9
|
*
|
|
12
10
|
* @param diff - Raw unified-diff content
|
|
13
11
|
* @param targetFile - Optional target file to scope extraction to
|
|
@@ -18,6 +16,18 @@
|
|
|
18
16
|
* binary new-file conflicts as unresolvable rather than auto-resolving.
|
|
19
17
|
*/
|
|
20
18
|
export declare function extractNewFileContentFromDiff(diff: string, targetFile?: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* {@link extractNewFileContentFromDiff} for many targets over one parse of
|
|
21
|
+
* the diff. Each target gets exactly what the single-target call returns;
|
|
22
|
+
* a target whose sections include a binary one (which that call refuses)
|
|
23
|
+
* is left out of the map and named in `refused`.
|
|
24
|
+
* @param diff - Raw unified-diff content
|
|
25
|
+
* @param targetFiles - Files whose created content to extract
|
|
26
|
+
*/
|
|
27
|
+
export declare function extractNewFileContentsFromDiff(diff: string, targetFiles: Iterable<string>): {
|
|
28
|
+
contents: Map<string, string>;
|
|
29
|
+
refused: string[];
|
|
30
|
+
};
|
|
21
31
|
/**
|
|
22
32
|
* Projects every text new file a diff creates into a path → content map, for
|
|
23
33
|
* the cross-patch lint projections that build a synthetic patch-queue entry.
|
|
@@ -39,15 +49,6 @@ export declare function extractNewFileContentFromDiff(diff: string, targetFile?:
|
|
|
39
49
|
* call sites previously did.
|
|
40
50
|
*/
|
|
41
51
|
export declare function buildNewFileTextProjection(diff: string): Map<string, string>;
|
|
42
|
-
/**
|
|
43
|
-
* Extracts the complete file content from a "new file" patch.
|
|
44
|
-
* When targetFile is provided, only extracts content for that file
|
|
45
|
-
* (required for multi-file patches).
|
|
46
|
-
* @param patchPath - Path to the patch file
|
|
47
|
-
* @param targetFile - Optional target file to scope extraction to
|
|
48
|
-
* @returns The file content that the patch would create
|
|
49
|
-
*/
|
|
50
|
-
export declare function extractNewFileContent(patchPath: string, targetFile?: string): Promise<string>;
|
|
51
52
|
/**
|
|
52
53
|
* Applies an already-read patch body to content. Batched callers that hold
|
|
53
54
|
* many (patch, file) pairs use this to read each patch body once instead of
|
|
@@ -4,14 +4,11 @@
|
|
|
4
4
|
* These operate on file content strings without filesystem side effects.
|
|
5
5
|
*/
|
|
6
6
|
import { PatchError } from '../errors/patch.js';
|
|
7
|
-
import { readText } from '../utils/fs.js';
|
|
8
7
|
import { isNewFileInPatch, parseDiffSections, parseHunksForFile } from './patch-parse.js';
|
|
9
8
|
/**
|
|
10
9
|
* Extracts the complete file content from a "new file" patch given a raw
|
|
11
|
-
* diff string already in memory.
|
|
12
|
-
*
|
|
13
|
-
* that already hold the diff (e.g. the in-flight export planner) and do
|
|
14
|
-
* not want to round-trip through the filesystem.
|
|
10
|
+
* diff string already in memory. A caller with only a patch file path
|
|
11
|
+
* reads it first; every caller holds the diff already.
|
|
15
12
|
*
|
|
16
13
|
* @param diff - Raw unified-diff content
|
|
17
14
|
* @param targetFile - Optional target file to scope extraction to
|
|
@@ -29,6 +26,33 @@ export function extractNewFileContentFromDiff(diff, targetFile) {
|
|
|
29
26
|
}
|
|
30
27
|
return contentFromSections(sections);
|
|
31
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* {@link extractNewFileContentFromDiff} for many targets over one parse of
|
|
31
|
+
* the diff. Each target gets exactly what the single-target call returns;
|
|
32
|
+
* a target whose sections include a binary one (which that call refuses)
|
|
33
|
+
* is left out of the map and named in `refused`.
|
|
34
|
+
* @param diff - Raw unified-diff content
|
|
35
|
+
* @param targetFiles - Files whose created content to extract
|
|
36
|
+
*/
|
|
37
|
+
export function extractNewFileContentsFromDiff(diff, targetFiles) {
|
|
38
|
+
const sectionsByTarget = new Map();
|
|
39
|
+
for (const section of parseDiffSections(diff)) {
|
|
40
|
+
const list = sectionsByTarget.get(section.targetPath) ?? [];
|
|
41
|
+
list.push(section);
|
|
42
|
+
sectionsByTarget.set(section.targetPath, list);
|
|
43
|
+
}
|
|
44
|
+
const contents = new Map();
|
|
45
|
+
const refused = [];
|
|
46
|
+
for (const target of targetFiles) {
|
|
47
|
+
const sections = sectionsByTarget.get(target) ?? [];
|
|
48
|
+
if (sections.some((section) => section.isBinary)) {
|
|
49
|
+
refused.push(target);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
contents.set(target, contentFromSections(sections));
|
|
53
|
+
}
|
|
54
|
+
return { contents, refused };
|
|
55
|
+
}
|
|
32
56
|
/**
|
|
33
57
|
* Walks the added (`+`) lines of already-parsed sections into file content.
|
|
34
58
|
* Shared by {@link extractNewFileContentFromDiff} and
|
|
@@ -89,18 +113,6 @@ export function buildNewFileTextProjection(diff) {
|
|
|
89
113
|
}
|
|
90
114
|
return newFiles;
|
|
91
115
|
}
|
|
92
|
-
/**
|
|
93
|
-
* Extracts the complete file content from a "new file" patch.
|
|
94
|
-
* When targetFile is provided, only extracts content for that file
|
|
95
|
-
* (required for multi-file patches).
|
|
96
|
-
* @param patchPath - Path to the patch file
|
|
97
|
-
* @param targetFile - Optional target file to scope extraction to
|
|
98
|
-
* @returns The file content that the patch would create
|
|
99
|
-
*/
|
|
100
|
-
export async function extractNewFileContent(patchPath, targetFile) {
|
|
101
|
-
const content = await readText(patchPath);
|
|
102
|
-
return extractNewFileContentFromDiff(content, targetFile);
|
|
103
|
-
}
|
|
104
116
|
/**
|
|
105
117
|
* Applies an already-read patch body to content. Batched callers that hold
|
|
106
118
|
* many (patch, file) pairs use this to read each patch body once instead of
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** A patch to replay, in queue order. */
|
|
2
|
+
export interface DryRunPatch {
|
|
3
|
+
filename: string;
|
|
4
|
+
path: string;
|
|
5
|
+
}
|
|
6
|
+
/** What the real rebase loop would do with one patch. */
|
|
7
|
+
export type DryRunVerdict = {
|
|
8
|
+
filename: string;
|
|
9
|
+
outcome: 'clean';
|
|
10
|
+
} | {
|
|
11
|
+
filename: string;
|
|
12
|
+
outcome: 'reduced-context';
|
|
13
|
+
step: number;
|
|
14
|
+
contextArg: string;
|
|
15
|
+
} | {
|
|
16
|
+
filename: string;
|
|
17
|
+
outcome: 'reject';
|
|
18
|
+
/** Files git named as not applying. Empty when stderr named none. */
|
|
19
|
+
files: string[];
|
|
20
|
+
/** git's own explanation, from the exact-context `--check`. */
|
|
21
|
+
detail: string;
|
|
22
|
+
};
|
|
23
|
+
/** Result of a full queue replay. */
|
|
24
|
+
export interface DryRunReplay {
|
|
25
|
+
verdicts: DryRunVerdict[];
|
|
26
|
+
/**
|
|
27
|
+
* Index of the first reject, or `undefined`. Verdicts after it were
|
|
28
|
+
* computed without that patch applied, so a reject there may cascade from
|
|
29
|
+
* the earlier one rather than be a conflict of its own.
|
|
30
|
+
*/
|
|
31
|
+
firstRejectIndex: number | undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parses the file names out of `git apply --check` failure output. Git
|
|
35
|
+
* reports a context mismatch as `error: patch failed: <file>:<line>` and
|
|
36
|
+
* `error: <file>: patch does not apply`, and a missing or pre-existing
|
|
37
|
+
* target as `error: <file>: does not exist in index` /
|
|
38
|
+
* `error: <file>: already exists in index`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseApplyCheckFailures(stderr: string): string[];
|
|
41
|
+
/**
|
|
42
|
+
* Replays `patches` onto the engine's HEAD tree in a private index.
|
|
43
|
+
* @param engineDir - Engine repository root
|
|
44
|
+
* @param patches - Patches in queue order
|
|
45
|
+
* @param maxFuzz - Maximum context-reduction steps, as for the real rebase
|
|
46
|
+
* @returns One verdict per patch
|
|
47
|
+
*/
|
|
48
|
+
export declare function replayQueueIndexOnly(engineDir: string, patches: readonly DryRunPatch[], maxFuzz: number): Promise<DryRunReplay>;
|