@hominis/fireforge 0.37.0 → 0.39.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 +33 -0
- package/README.md +1 -1
- package/dist/src/commands/build.js +13 -8
- package/dist/src/commands/config.js +1 -0
- package/dist/src/commands/discard.js +75 -50
- package/dist/src/commands/download.js +5 -2
- package/dist/src/commands/export-flow.d.ts +6 -2
- package/dist/src/commands/export-flow.js +17 -11
- package/dist/src/commands/export-placement-gate.js +4 -6
- package/dist/src/commands/export-placement-policy.d.ts +11 -3
- package/dist/src/commands/export-placement-policy.js +32 -29
- package/dist/src/commands/export-shared.js +17 -2
- package/dist/src/commands/export.js +5 -5
- package/dist/src/commands/furnace/index.js +23 -18
- package/dist/src/commands/import.js +5 -0
- package/dist/src/commands/lint-per-patch.js +15 -5
- package/dist/src/commands/lint.js +1 -1
- package/dist/src/commands/patch/lint-ignore.js +20 -1
- package/dist/src/commands/patch/move-files-create.d.ts +19 -0
- package/dist/src/commands/patch/move-files-create.js +116 -0
- package/dist/src/commands/patch/move-files-into.d.ts +47 -0
- package/dist/src/commands/patch/move-files-into.js +245 -0
- package/dist/src/commands/patch/move-files.d.ts +3 -1
- package/dist/src/commands/patch/move-files.js +74 -15
- package/dist/src/commands/patch/rename.js +4 -1
- package/dist/src/commands/patch/split.d.ts +11 -1
- package/dist/src/commands/patch/split.js +6 -5
- package/dist/src/commands/re-export-scan.d.ts +17 -0
- package/dist/src/commands/re-export-scan.js +51 -0
- package/dist/src/commands/re-export.js +17 -6
- package/dist/src/commands/reset.js +5 -0
- package/dist/src/commands/source.js +28 -3
- package/dist/src/commands/status-output.d.ts +9 -0
- package/dist/src/commands/status-output.js +33 -1
- package/dist/src/commands/status.js +17 -3
- package/dist/src/commands/test-diagnose.d.ts +1 -1
- package/dist/src/commands/test-diagnose.js +9 -3
- package/dist/src/commands/test-modes.d.ts +21 -0
- package/dist/src/commands/test-modes.js +42 -0
- package/dist/src/commands/test-register.js +6 -5
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +8 -2
- package/dist/src/commands/test-stale-gate.d.ts +44 -0
- package/dist/src/commands/test-stale-gate.js +81 -0
- package/dist/src/commands/test.js +52 -81
- package/dist/src/core/build-audit.d.ts +12 -0
- package/dist/src/core/build-audit.js +14 -0
- package/dist/src/core/build-baseline-types.d.ts +41 -0
- package/dist/src/core/build-baseline.d.ts +6 -1
- package/dist/src/core/build-baseline.js +48 -7
- package/dist/src/core/build-prepare.d.ts +9 -0
- package/dist/src/core/build-prepare.js +24 -0
- 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 +14 -1
- package/dist/src/core/discard-baseline.d.ts +60 -0
- package/dist/src/core/discard-baseline.js +232 -0
- package/dist/src/core/engine-session-lock.d.ts +10 -1
- package/dist/src/core/engine-session-lock.js +28 -2
- package/dist/src/core/file-lock.d.ts +33 -0
- package/dist/src/core/file-lock.js +32 -6
- package/dist/src/core/firefox-archive.d.ts +2 -1
- package/dist/src/core/firefox-archive.js +32 -8
- package/dist/src/core/firefox.d.ts +8 -3
- package/dist/src/core/firefox.js +11 -6
- package/dist/src/core/furnace-validate-helpers.js +28 -1
- package/dist/src/core/license-headers.d.ts +24 -0
- package/dist/src/core/license-headers.js +103 -1
- package/dist/src/core/patch-export.d.ts +10 -1
- package/dist/src/core/patch-export.js +12 -2
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -1
- package/dist/src/core/patch-lint-checkjs.js +30 -14
- package/dist/src/core/patch-lint-cross.d.ts +9 -1
- package/dist/src/core/patch-lint-cross.js +69 -9
- package/dist/src/core/patch-lint-mozbuild.d.ts +18 -0
- package/dist/src/core/patch-lint-mozbuild.js +104 -0
- package/dist/src/core/patch-lint.d.ts +3 -0
- package/dist/src/core/patch-lint.js +29 -8
- package/dist/src/core/patch-policy.d.ts +14 -1
- package/dist/src/core/patch-policy.js +13 -2
- package/dist/src/core/status-classify.d.ts +11 -2
- package/dist/src/core/status-classify.js +49 -25
- package/dist/src/core/test-harness-crash.d.ts +10 -0
- package/dist/src/core/test-harness-crash.js +16 -0
- package/dist/src/core/test-stale-check.d.ts +56 -1
- package/dist/src/core/test-stale-check.js +114 -4
- package/dist/src/core/token-manager.js +39 -2
- package/dist/src/core/token-variant.d.ts +7 -0
- package/dist/src/core/token-variant.js +9 -0
- package/dist/src/core/typecheck-shim.d.ts +13 -0
- package/dist/src/core/typecheck-shim.js +17 -2
- package/dist/src/core/typecheck.js +14 -3
- package/dist/src/types/commands/options.d.ts +69 -3
- package/dist/src/types/config.d.ts +22 -0
- package/dist/src/utils/options.d.ts +20 -0
- package/dist/src/utils/options.js +39 -0
- package/dist/src/utils/validation.d.ts +5 -0
- package/dist/src/utils/validation.js +7 -0
- package/package.json +3 -3
|
@@ -34,6 +34,11 @@ export declare function getLicenseHeader(license: ProjectLicense, style: Comment
|
|
|
34
34
|
* on lines 1–2 with the MPL header on lines 3+, which the raw
|
|
35
35
|
* `startsWith` check would otherwise miss.
|
|
36
36
|
*
|
|
37
|
+
* The MPL-2.0 header is additionally matched on normalized whitespace
|
|
38
|
+
* (see {@link normalizeCommentHead}) so upstream files using the older
|
|
39
|
+
* Mozilla wrap — breaking after "file," instead of "with this" — are
|
|
40
|
+
* accepted too; only the wrap position differs, never the wording.
|
|
41
|
+
*
|
|
37
42
|
* @param content - File content to check
|
|
38
43
|
* @param style - Comment syntax of the file
|
|
39
44
|
*/
|
|
@@ -51,6 +56,13 @@ export declare function hasAnyLicenseHeader(content: string, style: CommentStyle
|
|
|
51
56
|
* patch-lint, making it dead code for every other project license and
|
|
52
57
|
* forcing repo-side audit workarounds.
|
|
53
58
|
*
|
|
59
|
+
* Matching is wrap-agnostic: after the exact `startsWith` fast path, the
|
|
60
|
+
* leading block comment is compared on normalized whitespace (see
|
|
61
|
+
* {@link normalizeCommentHead}) so the older upstream wrap — breaking
|
|
62
|
+
* after "file," instead of "with this", as `ext-browser.js` ships —
|
|
63
|
+
* passes too. Only the line-break position may differ; altered wording
|
|
64
|
+
* still rejects.
|
|
65
|
+
*
|
|
54
66
|
* @param content - File content to check
|
|
55
67
|
*/
|
|
56
68
|
export declare function hasUpstreamMplBlockHeader(content: string): boolean;
|
|
@@ -69,6 +81,18 @@ export declare function hasAnyLicenseHeaderAnyStyle(content: string): boolean;
|
|
|
69
81
|
* @param maxLines - Number of leading lines to inspect (default 10)
|
|
70
82
|
*/
|
|
71
83
|
export declare function containsUpstreamLicenseText(content: string, maxLines?: number): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Returns true when the head of `content` carries a recognized THIRD-PARTY
|
|
86
|
+
* permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0), in any
|
|
87
|
+
* comment style. Used by `export` to treat such files as vendored: offering
|
|
88
|
+
* to prepend the project's own license header onto a byte-identity-pinned
|
|
89
|
+
* upstream bundle would mislicense third-party code (FORGE F15).
|
|
90
|
+
*
|
|
91
|
+
* @param content - File content to check
|
|
92
|
+
* @param maxLines - Number of leading lines to inspect (default 30 — full
|
|
93
|
+
* MIT/BSD license texts run longer than the 10-line project-header scan)
|
|
94
|
+
*/
|
|
95
|
+
export declare function hasThirdPartyPermissiveBanner(content: string, maxLines?: number): boolean;
|
|
72
96
|
/**
|
|
73
97
|
* Prepends the license header to a file on disk if it is not already present.
|
|
74
98
|
*
|
|
@@ -86,6 +86,59 @@ function stripLeadingEditorDirectives(content) {
|
|
|
86
86
|
} while (result !== prev);
|
|
87
87
|
return result;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Collapses all whitespace runs to single spaces and trims, so header
|
|
91
|
+
* comparisons ignore the exact line-break/wrap positions.
|
|
92
|
+
*/
|
|
93
|
+
function normalizeText(text) {
|
|
94
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The canonical upstream MPL-2.0 notice as a single normalized sentence
|
|
98
|
+
* (including the `http://mozilla.org/MPL/2.0/.` URL). Wrap-agnostic
|
|
99
|
+
* header checks match against this instead of an exact rendering.
|
|
100
|
+
*/
|
|
101
|
+
const MPL_HEADER_NORMALIZED = normalizeText(HEADER_LINES['MPL-2.0'].join(' '));
|
|
102
|
+
/**
|
|
103
|
+
* Extracts ONLY the leading comment span of `content` for the given
|
|
104
|
+
* comment style and returns it whitespace-normalized:
|
|
105
|
+
*
|
|
106
|
+
* - `js`: a leading run of `//` lines (markers stripped), or — when the
|
|
107
|
+
* file opens with a block comment instead — the first
|
|
108
|
+
* `/* … *\/` block;
|
|
109
|
+
* - `css`: the first `/* … *\/` block, with the ` * ` continuation-line
|
|
110
|
+
* prefixes stripped (same collapsing
|
|
111
|
+
* {@link normalizeLicenseHeadForScan} does);
|
|
112
|
+
* - `hash`: a leading run of `#` lines (markers stripped).
|
|
113
|
+
*
|
|
114
|
+
* A non-comment first line yields `''`. This lets callers accept a known
|
|
115
|
+
* header text regardless of where upstream wrapped its lines (e.g. the
|
|
116
|
+
* older Mozilla wrap that breaks after "file,"), without ever matching
|
|
117
|
+
* text beyond the leading comment.
|
|
118
|
+
*/
|
|
119
|
+
function normalizeCommentHead(content, style) {
|
|
120
|
+
const src = content.replace(/\r\n?/g, '\n');
|
|
121
|
+
const collectLineRun = (marker) => {
|
|
122
|
+
const parts = [];
|
|
123
|
+
for (const line of src.split('\n')) {
|
|
124
|
+
const m = marker.exec(line);
|
|
125
|
+
if (!m)
|
|
126
|
+
break;
|
|
127
|
+
parts.push(m[1] ?? '');
|
|
128
|
+
}
|
|
129
|
+
return normalizeText(parts.join(' '));
|
|
130
|
+
};
|
|
131
|
+
if (style === 'hash') {
|
|
132
|
+
return collectLineRun(/^[ \t]*#[ \t]?(.*)$/);
|
|
133
|
+
}
|
|
134
|
+
if (style === 'js' && /^[ \t]*\/\//.test(src)) {
|
|
135
|
+
return collectLineRun(/^[ \t]*\/\/[ \t]?(.*)$/);
|
|
136
|
+
}
|
|
137
|
+
const block = /^[ \t]*\/\*([\s\S]*?)\*\//.exec(src);
|
|
138
|
+
if (!block)
|
|
139
|
+
return '';
|
|
140
|
+
return normalizeText((block[1] ?? '').replace(/\n[ \t]*\*[ \t]*/g, ' '));
|
|
141
|
+
}
|
|
89
142
|
/**
|
|
90
143
|
* Returns true if `content` starts with any known license header for the
|
|
91
144
|
* given comment style.
|
|
@@ -103,6 +156,11 @@ function stripLeadingEditorDirectives(content) {
|
|
|
103
156
|
* on lines 1–2 with the MPL header on lines 3+, which the raw
|
|
104
157
|
* `startsWith` check would otherwise miss.
|
|
105
158
|
*
|
|
159
|
+
* The MPL-2.0 header is additionally matched on normalized whitespace
|
|
160
|
+
* (see {@link normalizeCommentHead}) so upstream files using the older
|
|
161
|
+
* Mozilla wrap — breaking after "file," instead of "with this" — are
|
|
162
|
+
* accepted too; only the wrap position differs, never the wording.
|
|
163
|
+
*
|
|
106
164
|
* @param content - File content to check
|
|
107
165
|
* @param style - Comment syntax of the file
|
|
108
166
|
*/
|
|
@@ -116,6 +174,9 @@ export function hasAnyLicenseHeader(content, style) {
|
|
|
116
174
|
if (style === 'js' && candidate.startsWith(getLicenseHeader('MPL-2.0', 'css'))) {
|
|
117
175
|
return true;
|
|
118
176
|
}
|
|
177
|
+
if (normalizeCommentHead(candidate, style).startsWith(MPL_HEADER_NORMALIZED)) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
119
180
|
}
|
|
120
181
|
return false;
|
|
121
182
|
}
|
|
@@ -132,11 +193,25 @@ export function hasAnyLicenseHeader(content, style) {
|
|
|
132
193
|
* patch-lint, making it dead code for every other project license and
|
|
133
194
|
* forcing repo-side audit workarounds.
|
|
134
195
|
*
|
|
196
|
+
* Matching is wrap-agnostic: after the exact `startsWith` fast path, the
|
|
197
|
+
* leading block comment is compared on normalized whitespace (see
|
|
198
|
+
* {@link normalizeCommentHead}) so the older upstream wrap — breaking
|
|
199
|
+
* after "file," instead of "with this", as `ext-browser.js` ships —
|
|
200
|
+
* passes too. Only the line-break position may differ; altered wording
|
|
201
|
+
* still rejects.
|
|
202
|
+
*
|
|
135
203
|
* @param content - File content to check
|
|
136
204
|
*/
|
|
137
205
|
export function hasUpstreamMplBlockHeader(content) {
|
|
138
206
|
const blockHeader = getLicenseHeader('MPL-2.0', 'css');
|
|
139
|
-
|
|
207
|
+
const candidates = [content, stripLeadingEditorDirectives(content)];
|
|
208
|
+
for (const candidate of candidates) {
|
|
209
|
+
if (candidate.startsWith(blockHeader))
|
|
210
|
+
return true;
|
|
211
|
+
if (normalizeCommentHead(candidate, 'css').startsWith(MPL_HEADER_NORMALIZED))
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
140
215
|
}
|
|
141
216
|
/**
|
|
142
217
|
* Returns true if `content` starts with any known license header in any
|
|
@@ -177,6 +252,33 @@ export function containsUpstreamLicenseText(content, maxLines = 10) {
|
|
|
177
252
|
];
|
|
178
253
|
return markers.some((marker) => scanText.includes(marker));
|
|
179
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Returns true when the head of `content` carries a recognized THIRD-PARTY
|
|
257
|
+
* permissive license banner (MIT / ISC / BSD-2 / BSD-3 / Apache-2.0), in any
|
|
258
|
+
* comment style. Used by `export` to treat such files as vendored: offering
|
|
259
|
+
* to prepend the project's own license header onto a byte-identity-pinned
|
|
260
|
+
* upstream bundle would mislicense third-party code (FORGE F15).
|
|
261
|
+
*
|
|
262
|
+
* @param content - File content to check
|
|
263
|
+
* @param maxLines - Number of leading lines to inspect (default 30 — full
|
|
264
|
+
* MIT/BSD license texts run longer than the 10-line project-header scan)
|
|
265
|
+
*/
|
|
266
|
+
export function hasThirdPartyPermissiveBanner(content, maxLines = 30) {
|
|
267
|
+
const head = content.split('\n').slice(0, maxLines).join('\n');
|
|
268
|
+
const scanText = normalizeLicenseHeadForScan(head).toLowerCase();
|
|
269
|
+
const markers = [
|
|
270
|
+
'mit license',
|
|
271
|
+
'permission is hereby granted, free of charge',
|
|
272
|
+
'isc license',
|
|
273
|
+
'permission to use, copy, modify',
|
|
274
|
+
'redistribution and use in source and binary forms',
|
|
275
|
+
'apache license',
|
|
276
|
+
'licensed under the apache license',
|
|
277
|
+
];
|
|
278
|
+
if (markers.some((marker) => scanText.includes(marker)))
|
|
279
|
+
return true;
|
|
280
|
+
return /spdx-license-identifier:\s*(mit|isc|bsd-2-clause|bsd-3-clause|apache-2\.0)\b/.test(scanText);
|
|
281
|
+
}
|
|
180
282
|
/**
|
|
181
283
|
* Prepends the license header to a file on disk if it is not already present.
|
|
182
284
|
*
|
|
@@ -30,7 +30,16 @@ export declare function sanitizeName(name: string): string;
|
|
|
30
30
|
* double-prefix incident) also collapses. Exported for direct testing.
|
|
31
31
|
*/
|
|
32
32
|
export declare function stripRedundantCategoryPrefix(sanitizedName: string, category: string): string;
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Sanitizes a patch name and drops redundant category/full-stem prefixes.
|
|
35
|
+
*
|
|
36
|
+
* A single trailing `.patch` extension is stripped before sanitisation:
|
|
37
|
+
* operators frequently pass a full patch FILENAME where a name is expected
|
|
38
|
+
* (`move-files <from> 348-ui-foo.patch --create`, `rename --to foo.patch`),
|
|
39
|
+
* and 0.38.0 slugged the extension into `-patch`, producing double-suffixed
|
|
40
|
+
* `...-patch.patch` files. A name that deliberately ends in a literal
|
|
41
|
+
* `-patch` slug segment must now be written without the dot form.
|
|
42
|
+
*/
|
|
34
43
|
export declare function patchNameSlug(name: string, category: string): string;
|
|
35
44
|
/**
|
|
36
45
|
* Generates the next patch filename with category.
|
|
@@ -75,9 +75,19 @@ export function stripRedundantCategoryPrefix(sanitizedName, category) {
|
|
|
75
75
|
}
|
|
76
76
|
return stripped.length > 0 ? stripped : sanitizedName;
|
|
77
77
|
}
|
|
78
|
-
/**
|
|
78
|
+
/**
|
|
79
|
+
* Sanitizes a patch name and drops redundant category/full-stem prefixes.
|
|
80
|
+
*
|
|
81
|
+
* A single trailing `.patch` extension is stripped before sanitisation:
|
|
82
|
+
* operators frequently pass a full patch FILENAME where a name is expected
|
|
83
|
+
* (`move-files <from> 348-ui-foo.patch --create`, `rename --to foo.patch`),
|
|
84
|
+
* and 0.38.0 slugged the extension into `-patch`, producing double-suffixed
|
|
85
|
+
* `...-patch.patch` files. A name that deliberately ends in a literal
|
|
86
|
+
* `-patch` slug segment must now be written without the dot form.
|
|
87
|
+
*/
|
|
79
88
|
export function patchNameSlug(name, category) {
|
|
80
|
-
|
|
89
|
+
const stem = name.replace(/\.patch$/i, '');
|
|
90
|
+
return stripRedundantCategoryPrefix(sanitizeName(stem), category);
|
|
81
91
|
}
|
|
82
92
|
/**
|
|
83
93
|
* Generates the next patch filename with category.
|
|
@@ -25,11 +25,16 @@
|
|
|
25
25
|
* while reporting only the patch under export.
|
|
26
26
|
*/
|
|
27
27
|
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
28
|
-
import type { PatchLintCheckJsCompilerOptions, PatchLintConfig } from '../types/config.js';
|
|
28
|
+
import type { PatchLintCheckJsCompilerOptions, PatchLintConfig, PatchLintSeverityGate } from '../types/config.js';
|
|
29
29
|
/** How a checkJs run controls reporting and resolution; see module docs. */
|
|
30
30
|
export interface CheckJsMode {
|
|
31
31
|
strict: boolean;
|
|
32
32
|
compilerOptions?: PatchLintCheckJsCompilerOptions;
|
|
33
|
+
/**
|
|
34
|
+
* How to report undefined free identifiers (TS2304/TS2552). Default
|
|
35
|
+
* 'warning' — see `patchLint.undefinedIdentifiers` (FORGE F12).
|
|
36
|
+
*/
|
|
37
|
+
undefinedIdentifiers?: PatchLintSeverityGate;
|
|
33
38
|
}
|
|
34
39
|
/**
|
|
35
40
|
* Result of {@link runCheckJsGrouped}: diagnostics attributed to the
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import { basename, resolve } from 'node:path';
|
|
29
29
|
import { pathExists } from '../utils/fs.js';
|
|
30
30
|
import { verbose } from '../utils/logger.js';
|
|
31
|
-
import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES } from './typecheck-shim.js';
|
|
31
|
+
import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
|
|
32
32
|
// ---------------------------------------------------------------------------
|
|
33
33
|
// Public API
|
|
34
34
|
// ---------------------------------------------------------------------------
|
|
@@ -312,7 +312,7 @@ export async function runCheckJsGrouped(repoDir, resolutionOwned, extraShimPath,
|
|
|
312
312
|
},
|
|
313
313
|
};
|
|
314
314
|
const program = ts.createProgram(rootFiles, options, host);
|
|
315
|
-
const byFile = groupOwnedDiagnostics(ts, [...program.getSemanticDiagnostics(), ...program.getSyntacticDiagnostics()], relByAbsolute);
|
|
315
|
+
const byFile = groupOwnedDiagnostics(ts, [...program.getSemanticDiagnostics(), ...program.getSyntacticDiagnostics()], relByAbsolute, mode?.undefinedIdentifiers ?? 'warning');
|
|
316
316
|
verbose(`checkJs: analyzed ${rootFiles.length - 1} file(s) across ${byFile.size} owning file(s)`);
|
|
317
317
|
return { byFile, global: [] };
|
|
318
318
|
}
|
|
@@ -323,11 +323,14 @@ export async function runCheckJsGrouped(repoDir, resolutionOwned, extraShimPath,
|
|
|
323
323
|
* or shim files are dropped — only owned files (in `relByAbsolute`) carry
|
|
324
324
|
* findings.
|
|
325
325
|
*/
|
|
326
|
-
function groupOwnedDiagnostics(ts, diagnostics, relByAbsolute) {
|
|
326
|
+
function groupOwnedDiagnostics(ts, diagnostics, relByAbsolute, undefinedIdentifiers = 'warning') {
|
|
327
327
|
const byFile = new Map();
|
|
328
328
|
for (const diag of diagnostics) {
|
|
329
329
|
if (SUPPRESSED_DIAGNOSTIC_CODES.has(diag.code))
|
|
330
330
|
continue;
|
|
331
|
+
const isUndefinedIdentifier = UNDEFINED_IDENTIFIER_CODES.has(diag.code);
|
|
332
|
+
if (isUndefinedIdentifier && undefinedIdentifiers === 'off')
|
|
333
|
+
continue;
|
|
331
334
|
const sourceFile = diag.file;
|
|
332
335
|
if (!sourceFile)
|
|
333
336
|
continue;
|
|
@@ -339,12 +342,20 @@ function groupOwnedDiagnostics(ts, diagnostics, relByAbsolute) {
|
|
|
339
342
|
const messageText = typeof diag.messageText === 'string'
|
|
340
343
|
? diag.messageText
|
|
341
344
|
: ts.flattenDiagnosticMessageText(diag.messageText, '\n');
|
|
342
|
-
const severity =
|
|
345
|
+
const severity = isUndefinedIdentifier
|
|
346
|
+
? undefinedIdentifiers === 'error'
|
|
347
|
+
? 'error'
|
|
348
|
+
: 'warning'
|
|
349
|
+
: diag.category === ts.DiagnosticCategory.Error
|
|
350
|
+
? 'error'
|
|
351
|
+
: 'warning';
|
|
343
352
|
const bucket = byFile.get(relPath) ?? [];
|
|
344
353
|
bucket.push({
|
|
345
354
|
file: relPath,
|
|
346
355
|
check: 'checkjs-type-error',
|
|
347
|
-
message:
|
|
356
|
+
message: isUndefinedIdentifier
|
|
357
|
+
? `Line ${line}: ${messageText} ${UNDEFINED_IDENTIFIER_HINT}`
|
|
358
|
+
: `Line ${line}: ${messageText}`,
|
|
348
359
|
severity,
|
|
349
360
|
});
|
|
350
361
|
byFile.set(relPath, bucket);
|
|
@@ -389,11 +400,20 @@ export async function runCheckJs(repoDir, patchOwnedFiles, extraShimPath, projec
|
|
|
389
400
|
* not surface other patches' diagnostics)
|
|
390
401
|
*/
|
|
391
402
|
export async function invokePatchLintCheckJs(repoDir, patchOwnedFiles, patchLint, projectRoot, reportScope) {
|
|
403
|
+
return runCheckJs(repoDir, patchOwnedFiles, patchLint.checkJsExtraShim, projectRoot, modeFromPatchLintConfig(patchLint), reportScope);
|
|
404
|
+
}
|
|
405
|
+
/** Derives the {@link CheckJsMode} from a `patchLint` config block. */
|
|
406
|
+
function modeFromPatchLintConfig(patchLint) {
|
|
392
407
|
const strict = patchLint.checkJsStrict === true;
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
408
|
+
return {
|
|
409
|
+
strict,
|
|
410
|
+
...(strict && patchLint.checkJsCompilerOptions
|
|
411
|
+
? { compilerOptions: patchLint.checkJsCompilerOptions }
|
|
412
|
+
: {}),
|
|
413
|
+
...(patchLint.undefinedIdentifiers !== undefined
|
|
414
|
+
? { undefinedIdentifiers: patchLint.undefinedIdentifiers }
|
|
415
|
+
: {}),
|
|
416
|
+
};
|
|
397
417
|
}
|
|
398
418
|
/**
|
|
399
419
|
* Grouped variant of {@link invokePatchLintCheckJs}: builds one queue-wide
|
|
@@ -408,9 +428,5 @@ export async function invokePatchLintCheckJs(repoDir, patchOwnedFiles, patchLint
|
|
|
408
428
|
* @param projectRoot - FireForge project root for shim resolution
|
|
409
429
|
*/
|
|
410
430
|
export async function invokePatchLintCheckJsGrouped(repoDir, patchOwnedFiles, patchLint, projectRoot) {
|
|
411
|
-
|
|
412
|
-
const mode = strict && patchLint.checkJsCompilerOptions
|
|
413
|
-
? { strict, compilerOptions: patchLint.checkJsCompilerOptions }
|
|
414
|
-
: { strict };
|
|
415
|
-
return runCheckJsGrouped(repoDir, patchOwnedFiles, patchLint.checkJsExtraShim, projectRoot, mode);
|
|
431
|
+
return runCheckJsGrouped(repoDir, patchOwnedFiles, patchLint.checkJsExtraShim, projectRoot, modeFromPatchLintConfig(patchLint));
|
|
416
432
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* single module.
|
|
11
11
|
*/
|
|
12
12
|
import type { PatchLintIssue, PatchMetadata } from '../types/commands/index.js';
|
|
13
|
+
import type { FireForgeConfig, PatchPolicyConfig } from '../types/config.js';
|
|
13
14
|
/**
|
|
14
15
|
* One patch's contribution to {@link PatchQueueContext}.
|
|
15
16
|
*
|
|
@@ -57,6 +58,13 @@ export interface PatchQueueEntry {
|
|
|
57
58
|
export interface PatchQueueContext {
|
|
58
59
|
/** Entries in application order (lowest `order` first). */
|
|
59
60
|
entries: PatchQueueEntry[];
|
|
61
|
+
/**
|
|
62
|
+
* Optional patchPolicy config. When present, the forward-import rule
|
|
63
|
+
* validates its "Closest legal ordinal" suggestion against the importing
|
|
64
|
+
* patch's category range and suppresses it when no legal ordinal exists
|
|
65
|
+
* (FORGE F14). Absent = current behavior (hint always printed).
|
|
66
|
+
*/
|
|
67
|
+
patchPolicy?: PatchPolicyConfig;
|
|
60
68
|
}
|
|
61
69
|
/**
|
|
62
70
|
* Builds a {@link PatchQueueContext} by reading every .patch file in the
|
|
@@ -70,7 +78,7 @@ export interface PatchQueueContext {
|
|
|
70
78
|
*
|
|
71
79
|
* @param patchesDir - Path to the patches directory
|
|
72
80
|
*/
|
|
73
|
-
export declare function buildPatchQueueContext(patchesDir: string): Promise<PatchQueueContext>;
|
|
81
|
+
export declare function buildPatchQueueContext(patchesDir: string, config?: FireForgeConfig): Promise<PatchQueueContext>;
|
|
74
82
|
/**
|
|
75
83
|
* Returns the raw `path → patches[]` map of files created in `new file
|
|
76
84
|
* mode` by at least one patch in the queue. Paths created by only one
|
|
@@ -18,6 +18,7 @@ import { stripJsComments } from '../utils/regex.js';
|
|
|
18
18
|
import { discoverPatches } from './patch-files.js';
|
|
19
19
|
import { detectNewFilesInDiff, extractAddedLinesPerFile } from './patch-lint-diff.js';
|
|
20
20
|
import { loadPatchesManifest } from './patch-manifest-io.js';
|
|
21
|
+
import { categoryRangeForOrder, categoryRangeLabel } from './patch-policy.js';
|
|
21
22
|
import { extractNewFileContent } from './patch-transform.js';
|
|
22
23
|
/**
|
|
23
24
|
* Builds a {@link PatchQueueContext} by reading every .patch file in the
|
|
@@ -31,7 +32,7 @@ import { extractNewFileContent } from './patch-transform.js';
|
|
|
31
32
|
*
|
|
32
33
|
* @param patchesDir - Path to the patches directory
|
|
33
34
|
*/
|
|
34
|
-
export async function buildPatchQueueContext(patchesDir) {
|
|
35
|
+
export async function buildPatchQueueContext(patchesDir, config) {
|
|
35
36
|
const patches = await discoverPatches(patchesDir);
|
|
36
37
|
const manifest = await loadPatchesManifest(patchesDir);
|
|
37
38
|
const metadataByFilename = new Map();
|
|
@@ -78,7 +79,7 @@ export async function buildPatchQueueContext(patchesDir) {
|
|
|
78
79
|
}
|
|
79
80
|
// Sort by order so rules can rely on entries being in apply order.
|
|
80
81
|
entries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
81
|
-
return { entries };
|
|
82
|
+
return { entries, ...(config?.patchPolicy ? { patchPolicy: config.patchPolicy } : {}) };
|
|
82
83
|
}
|
|
83
84
|
/**
|
|
84
85
|
* Returns the raw `path → patches[]` map of files created in `new file
|
|
@@ -292,6 +293,34 @@ export function extractImportSpecifiersWithLines(source) {
|
|
|
292
293
|
collectGetterSpecifiers(stripped, results, offsetToLine);
|
|
293
294
|
return results;
|
|
294
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Bare `Name: "url"` property lines added inside a PRE-EXISTING
|
|
298
|
+
* `defineESModuleGetters` (or `XPCOMUtils` lazy-getter) map. Applied ONLY to
|
|
299
|
+
* `modifiedFileAdditions`: a patch that inserts one property into an
|
|
300
|
+
* existing getter object never carries the `defineESModuleGetters(` opener
|
|
301
|
+
* in its added lines, so the balanced-brace walk in
|
|
302
|
+
* {@link extractImportSpecifiersWithLines} cannot see it (FORGE F3). The
|
|
303
|
+
* URL-scheme restriction keeps ordinary object literals
|
|
304
|
+
* (`label: "Foo.sys.mjs"`) out; the leaf-extension filter downstream does
|
|
305
|
+
* the rest.
|
|
306
|
+
*/
|
|
307
|
+
const BARE_GETTER_PROPERTY_LINE = /^\s*(?:["'])?[A-Za-z_$][\w$]*(?:["'])?\s*:\s*["']((?:resource|chrome|moz-src):\/\/[^"']+)["']\s*,?\s*$/;
|
|
308
|
+
/**
|
|
309
|
+
* Extracts specifiers from bare getter-property lines in added-lines-only
|
|
310
|
+
* content. Comment stripping preserves line alignment, so the reported line
|
|
311
|
+
* numbers match what {@link findForwardImportIgnoreLines} walks.
|
|
312
|
+
*/
|
|
313
|
+
function extractBareGetterSpecifiers(source) {
|
|
314
|
+
const stripped = stripJsComments(source);
|
|
315
|
+
const results = [];
|
|
316
|
+
const lines = stripped.split('\n');
|
|
317
|
+
for (let i = 0; i < lines.length; i++) {
|
|
318
|
+
const match = BARE_GETTER_PROPERTY_LINE.exec(lines[i] ?? '');
|
|
319
|
+
if (match?.[1])
|
|
320
|
+
results.push({ specifier: match[1], line: i });
|
|
321
|
+
}
|
|
322
|
+
return results;
|
|
323
|
+
}
|
|
295
324
|
/**
|
|
296
325
|
* Marker comment operators can use to suppress the forward-import rule
|
|
297
326
|
* for imports that resolve to a basename false positive (two unrelated
|
|
@@ -376,11 +405,22 @@ function buildNewFileIndex(ctx) {
|
|
|
376
405
|
* pre-existing HEAD imports that happen to collide with a later-created file.
|
|
377
406
|
*/
|
|
378
407
|
function eachForwardImportSite(ctx, newFileIndex, visit) {
|
|
379
|
-
const checkSite = (entry, sitePath, content) => {
|
|
408
|
+
const checkSite = (entry, sitePath, content, includeBareGetterLines = false) => {
|
|
380
409
|
if (!isForwardImportableFile(sitePath))
|
|
381
410
|
return;
|
|
411
|
+
const sites = extractImportSpecifiersWithLines(content);
|
|
412
|
+
if (includeBareGetterLines) {
|
|
413
|
+
// Dedupe by (specifier, line): a patch that adds the whole getter
|
|
414
|
+
// call carries both the opener (found by the balanced walk) and the
|
|
415
|
+
// property line (found here) — report each site once.
|
|
416
|
+
const seen = new Set(sites.map((site) => `${site.specifier}|${String(site.line)}`));
|
|
417
|
+
for (const extra of extractBareGetterSpecifiers(content)) {
|
|
418
|
+
if (!seen.has(`${extra.specifier}|${String(extra.line)}`))
|
|
419
|
+
sites.push(extra);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
382
422
|
const ignoreLines = findForwardImportIgnoreLines(content);
|
|
383
|
-
for (const { specifier, line } of
|
|
423
|
+
for (const { specifier, line } of sites) {
|
|
384
424
|
if (ignoreLines.has(line))
|
|
385
425
|
continue;
|
|
386
426
|
const cleaned = specifier.split(/[?#]/)[0] ?? specifier;
|
|
@@ -400,7 +440,7 @@ function eachForwardImportSite(ctx, newFileIndex, visit) {
|
|
|
400
440
|
for (const [path, content] of entry.newFiles)
|
|
401
441
|
checkSite(entry, path, content);
|
|
402
442
|
for (const [path, added] of entry.modifiedFileAdditions)
|
|
403
|
-
checkSite(entry, path, added);
|
|
443
|
+
checkSite(entry, path, added, true);
|
|
404
444
|
}
|
|
405
445
|
}
|
|
406
446
|
/**
|
|
@@ -454,8 +494,28 @@ export function lintPatchQueueForwardImports(ctx) {
|
|
|
454
494
|
.join(',');
|
|
455
495
|
// Lowest ordinal that lands AFTER every later-ordered creator —
|
|
456
496
|
// turns the operator's "guess and re-run" loop into a single shot
|
|
457
|
-
// when the only fix is reordering.
|
|
497
|
+
// when the only fix is reordering. When patchPolicy is available and
|
|
498
|
+
// that ordinal falls outside the importing patch's category range,
|
|
499
|
+
// `patch reorder` would refuse it — suppress the hint and point at
|
|
500
|
+
// the staged-dependency remedy instead (FORGE F14).
|
|
458
501
|
const suggestedOrder = Math.max(...laterOwners.map((o) => o.order)) + 1;
|
|
502
|
+
const category = entry.metadata?.category ?? /^\d+-([a-z]+)-/.exec(entry.filename)?.[1];
|
|
503
|
+
let ordinalHint = `Closest legal ordinal that satisfies this dependency: ${suggestedOrder}.`;
|
|
504
|
+
if (ctx.patchPolicy &&
|
|
505
|
+
category !== undefined &&
|
|
506
|
+
categoryRangeForOrder(ctx.patchPolicy, category, suggestedOrder) === null) {
|
|
507
|
+
const label = categoryRangeLabel(ctx.patchPolicy.ranges, category);
|
|
508
|
+
ordinalHint =
|
|
509
|
+
`No legal ordinal in the ${category} category range (${label}) lands after the ` +
|
|
510
|
+
'creating patch(es); declaring the staged dependency is the recommended remedy.';
|
|
511
|
+
}
|
|
512
|
+
// The exact per-owner invocation turns the refusal into a paste-and-run
|
|
513
|
+
// remedy instead of a flag-discovery exercise (FORGE F3).
|
|
514
|
+
const stagedCommands = laterOwners
|
|
515
|
+
.map((o) => `fireforge patch staged-dependency ${entry.filename} --add ` +
|
|
516
|
+
`--file ${sitePath} --specifier "${specifier}" ` +
|
|
517
|
+
`--creates ${o.fullPath} --owner ${o.filename}`)
|
|
518
|
+
.join('; ');
|
|
459
519
|
issues.push({
|
|
460
520
|
file: sitePath,
|
|
461
521
|
check: 'forward-import',
|
|
@@ -463,10 +523,10 @@ export function lintPatchQueueForwardImports(ctx) {
|
|
|
463
523
|
message: `${sitePath} in ${entry.filename} imports "${specifier}", ` +
|
|
464
524
|
`but the matching new file is created by a later patch: ${ownersSummary}. ` +
|
|
465
525
|
'Reorder the patches so the dependency is created first, move the import ' +
|
|
466
|
-
'into the later patch, declare the intentional staged dependency with ' +
|
|
467
|
-
|
|
526
|
+
'into the later patch, declare the intentional staged dependency with: ' +
|
|
527
|
+
`${stagedCommands}; or mark the import with ` +
|
|
468
528
|
`"// ${FORWARD_IMPORT_IGNORE_MARKER}" if the basename collision is a false positive. ` +
|
|
469
|
-
|
|
529
|
+
ordinalHint,
|
|
470
530
|
severity: 'error',
|
|
471
531
|
});
|
|
472
532
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-patch lint check for moz.build sorted-list violations (FORGE F2).
|
|
3
|
+
*
|
|
4
|
+
* mozbuild enforces alphabetical ordering on `StrictOrderingOnAppendList`
|
|
5
|
+
* variables and raises `mozbuild.util.UnsortedError: … expected "X" but got
|
|
6
|
+
* "Y"` — but only at CONFIGURE time, i.e. minutes into a `fireforge build`
|
|
7
|
+
* or `fireforge test --build` dispatch. This check reads the patched
|
|
8
|
+
* `moz.build` files from the engine tree (the working tree carries the
|
|
9
|
+
* applied patch state, same as every other per-patch rule) and reports the
|
|
10
|
+
* exact expected/got pair before any build starts.
|
|
11
|
+
*/
|
|
12
|
+
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
13
|
+
/**
|
|
14
|
+
* Lints the patched content of every `moz.build` file the patch touches for
|
|
15
|
+
* unsorted strict-ordered lists, mirroring mozbuild's `UnsortedError`
|
|
16
|
+
* report: the first index where the list diverges from its sorted copy.
|
|
17
|
+
*/
|
|
18
|
+
export declare function lintMozBuildSortedLists(repoDir: string, affectedFiles: string[]): Promise<PatchLintIssue[]>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Per-patch lint check for moz.build sorted-list violations (FORGE F2).
|
|
4
|
+
*
|
|
5
|
+
* mozbuild enforces alphabetical ordering on `StrictOrderingOnAppendList`
|
|
6
|
+
* variables and raises `mozbuild.util.UnsortedError: … expected "X" but got
|
|
7
|
+
* "Y"` — but only at CONFIGURE time, i.e. minutes into a `fireforge build`
|
|
8
|
+
* or `fireforge test --build` dispatch. This check reads the patched
|
|
9
|
+
* `moz.build` files from the engine tree (the working tree carries the
|
|
10
|
+
* applied patch state, same as every other per-patch rule) and reports the
|
|
11
|
+
* exact expected/got pair before any build starts.
|
|
12
|
+
*/
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { pathExists, readText } from '../utils/fs.js';
|
|
15
|
+
import { mozbuildSortCompare } from './manifest-helpers.js';
|
|
16
|
+
/**
|
|
17
|
+
* moz.build list variables mozbuild declares as `StrictOrderingOnAppendList`
|
|
18
|
+
* (python/mozbuild/mozbuild/frontend/context.py). configure raises
|
|
19
|
+
* `UnsortedError` on append when out of order. `DIRS` is deliberately absent
|
|
20
|
+
* — directory order is meaningful and not strict-ordered. Dotted namespaces
|
|
21
|
+
* (`EXTRA_JS_MODULES.<subdir>`, `EXPORTS.<ns>`) inherit strict ordering
|
|
22
|
+
* from their parent variable.
|
|
23
|
+
*/
|
|
24
|
+
const STRICT_ORDERED_MOZBUILD_LISTS = [
|
|
25
|
+
'EXTRA_COMPONENTS',
|
|
26
|
+
'EXTRA_JS_MODULES',
|
|
27
|
+
'EXTRA_PP_COMPONENTS',
|
|
28
|
+
'EXPORTS',
|
|
29
|
+
'PYTHON_UNITTEST_MANIFESTS',
|
|
30
|
+
'TESTING_JS_MODULES',
|
|
31
|
+
'XPIDL_SOURCES',
|
|
32
|
+
];
|
|
33
|
+
const LIST_OPENER = new RegExp(`^\\s*((?:${STRICT_ORDERED_MOZBUILD_LISTS.join('|')})(?:\\.[A-Za-z_][A-Za-z0-9_]*)*)\\s*\\+?=\\s*\\[`);
|
|
34
|
+
/** Extracts every strict-ordered list (with items in file order) from moz.build content. */
|
|
35
|
+
function collectStrictOrderedLists(content) {
|
|
36
|
+
const lines = content.split('\n');
|
|
37
|
+
const occurrences = [];
|
|
38
|
+
for (let i = 0; i < lines.length; i++) {
|
|
39
|
+
const line = lines[i] ?? '';
|
|
40
|
+
const opener = LIST_OPENER.exec(line);
|
|
41
|
+
if (!opener)
|
|
42
|
+
continue;
|
|
43
|
+
const varName = opener[1] ?? '';
|
|
44
|
+
const items = [];
|
|
45
|
+
// Items may start on the opener line (`VAR += ["a", "b"]`) or span
|
|
46
|
+
// following lines until the closing bracket.
|
|
47
|
+
let rest = line.slice(opener[0].length);
|
|
48
|
+
let closed = false;
|
|
49
|
+
for (;;) {
|
|
50
|
+
for (const quoted of rest.matchAll(/["']([^"']+)["']/g)) {
|
|
51
|
+
items.push(quoted[1] ?? '');
|
|
52
|
+
}
|
|
53
|
+
if (/\]/.test(rest)) {
|
|
54
|
+
closed = true;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
i += 1;
|
|
58
|
+
if (i >= lines.length)
|
|
59
|
+
break;
|
|
60
|
+
rest = lines[i] ?? '';
|
|
61
|
+
if (/^\s*\]/.test(rest)) {
|
|
62
|
+
closed = true;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (closed)
|
|
67
|
+
occurrences.push({ varName, items });
|
|
68
|
+
}
|
|
69
|
+
return occurrences;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Lints the patched content of every `moz.build` file the patch touches for
|
|
73
|
+
* unsorted strict-ordered lists, mirroring mozbuild's `UnsortedError`
|
|
74
|
+
* report: the first index where the list diverges from its sorted copy.
|
|
75
|
+
*/
|
|
76
|
+
export async function lintMozBuildSortedLists(repoDir, affectedFiles) {
|
|
77
|
+
const issues = [];
|
|
78
|
+
for (const file of affectedFiles) {
|
|
79
|
+
if (!(file === 'moz.build' || file.endsWith('/moz.build')))
|
|
80
|
+
continue;
|
|
81
|
+
const filePath = join(repoDir, file);
|
|
82
|
+
if (!(await pathExists(filePath)))
|
|
83
|
+
continue;
|
|
84
|
+
const content = await readText(filePath);
|
|
85
|
+
for (const { varName, items } of collectStrictOrderedLists(content)) {
|
|
86
|
+
const sorted = [...items].sort(mozbuildSortCompare);
|
|
87
|
+
const divergence = items.findIndex((item, index) => item !== sorted[index]);
|
|
88
|
+
if (divergence === -1)
|
|
89
|
+
continue;
|
|
90
|
+
const expected = sorted[divergence] ?? '';
|
|
91
|
+
const got = items[divergence] ?? '';
|
|
92
|
+
issues.push({
|
|
93
|
+
file,
|
|
94
|
+
check: 'mozbuild-unsorted-list',
|
|
95
|
+
fingerprint: `mozbuild-unsorted-list|${file}|${varName}|${expected}|${got}`,
|
|
96
|
+
message: `moz.build list ${varName} is not alphabetically sorted: ` +
|
|
97
|
+
`expected "${expected}" but got "${got}" ` +
|
|
98
|
+
'(mozbuild will fail at configure time with UnsortedError)',
|
|
99
|
+
severity: 'error',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return issues;
|
|
104
|
+
}
|
|
@@ -8,6 +8,9 @@ export { lintPatchedCss };
|
|
|
8
8
|
* Counts the total lines in a unified diff and the number of non-binary
|
|
9
9
|
* text lines, so binary hunks do not inflate patch size checks.
|
|
10
10
|
*
|
|
11
|
+
* Line counting agrees with `wc -l`: a trailing newline does not add a
|
|
12
|
+
* phantom empty line to either `total` or the binary-hunk accounting.
|
|
13
|
+
*
|
|
11
14
|
* @param diffContent - Raw unified diff string
|
|
12
15
|
* @returns Object with `total` line count and `textLines` (total minus binary hunk lines)
|
|
13
16
|
*/
|