@hominis/fireforge 0.18.8 → 0.18.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/src/commands/lint.d.ts +36 -0
  2. package/dist/src/commands/lint.js +61 -1
  3. package/dist/src/commands/patch/index.d.ts +5 -3
  4. package/dist/src/commands/patch/index.js +8 -4
  5. package/dist/src/commands/patch/lint-ignore.d.ts +8 -0
  6. package/dist/src/commands/patch/lint-ignore.js +8 -4
  7. package/dist/src/commands/patch/rename.d.ts +36 -0
  8. package/dist/src/commands/patch/rename.js +244 -0
  9. package/dist/src/commands/test.js +50 -3
  10. package/dist/src/core/ast-utils.d.ts +5 -1
  11. package/dist/src/core/ast-utils.js +10 -3
  12. package/dist/src/core/build-audit-resolve.d.ts +5 -3
  13. package/dist/src/core/build-audit-resolve.js +12 -3
  14. package/dist/src/core/config-paths.d.ts +1 -1
  15. package/dist/src/core/config-paths.js +1 -0
  16. package/dist/src/core/config-validate.js +4 -0
  17. package/dist/src/core/license-headers.d.ts +5 -0
  18. package/dist/src/core/license-headers.js +46 -5
  19. package/dist/src/core/marionette-port.d.ts +29 -0
  20. package/dist/src/core/marionette-port.js +82 -0
  21. package/dist/src/core/patch-export.d.ts +10 -0
  22. package/dist/src/core/patch-export.js +8 -2
  23. package/dist/src/core/patch-lint-chrome-jsdoc.d.ts +47 -0
  24. package/dist/src/core/patch-lint-chrome-jsdoc.js +87 -0
  25. package/dist/src/core/patch-lint-cross.js +6 -1
  26. package/dist/src/core/patch-lint-jsdoc.d.ts +37 -0
  27. package/dist/src/core/patch-lint-jsdoc.js +24 -3
  28. package/dist/src/core/patch-lint-ownership.d.ts +21 -3
  29. package/dist/src/core/patch-lint-ownership.js +45 -18
  30. package/dist/src/core/patch-lint.d.ts +7 -2
  31. package/dist/src/core/patch-lint.js +24 -6
  32. package/dist/src/types/commands/index.d.ts +1 -1
  33. package/dist/src/types/commands/options.d.ts +36 -0
  34. package/dist/src/types/config.d.ts +12 -1
  35. package/package.json +1 -1
@@ -5,12 +5,19 @@ import { walk } from 'estree-walker';
5
5
  * Parse JavaScript source as a **script** (not an ES module).
6
6
  * All Mozilla chrome JS files (`browser-main.js`, `browser-init.js`,
7
7
  * `customElements.js`, etc.) are scripts that run in a privileged scope.
8
+ *
9
+ * @param content - Source text to parse
10
+ * @param onComment - Optional array that acorn fills with comment nodes
11
+ * @returns Parsed program AST with character-offset positions
8
12
  */
9
- export function parseScript(content) {
10
- return acorn.parse(content, {
13
+ export function parseScript(content, onComment) {
14
+ const opts = {
11
15
  sourceType: 'script',
12
16
  ecmaVersion: 'latest',
13
- });
17
+ };
18
+ if (onComment)
19
+ opts.onComment = onComment;
20
+ return acorn.parse(content, opts);
14
21
  }
15
22
  /**
16
23
  * Parse JavaScript source as an **ES module**.
@@ -2,9 +2,11 @@
2
2
  * Heuristic test for "this looks like a packaged-test source file" — the
3
3
  * audit routes such paths to `_tests/` instead of `dist/`. Matches
4
4
  * mochitest / xpcshell / browser-chrome conventions: any source under a
5
- * `/test/` or `/tests/` directory, or with a `browser_` / `test_` prefix
6
- * on a `.js`/`.toml` basename. Test manifests (`*.toml`, `*.list`,
7
- * `*.ini`) under those directories also qualify.
5
+ * `/test/` or `/tests/` directory, anywhere under a `testing/` subtree
6
+ * (which holds mochitest / marionette / xpcshell harness sources), or
7
+ * with a `browser_` / `test_` prefix on a `.js`/`.toml` basename. Test
8
+ * manifests (`*.toml`, `*.list`, `*.ini`) under those directories also
9
+ * qualify.
8
10
  *
9
11
  * @param sourcePath Engine-relative POSIX path
10
12
  * @returns True when the file belongs to the test tree, not the bundle
@@ -30,9 +30,11 @@ const MAX_SCAN_DEPTH = 12;
30
30
  * Heuristic test for "this looks like a packaged-test source file" — the
31
31
  * audit routes such paths to `_tests/` instead of `dist/`. Matches
32
32
  * mochitest / xpcshell / browser-chrome conventions: any source under a
33
- * `/test/` or `/tests/` directory, or with a `browser_` / `test_` prefix
34
- * on a `.js`/`.toml` basename. Test manifests (`*.toml`, `*.list`,
35
- * `*.ini`) under those directories also qualify.
33
+ * `/test/` or `/tests/` directory, anywhere under a `testing/` subtree
34
+ * (which holds mochitest / marionette / xpcshell harness sources), or
35
+ * with a `browser_` / `test_` prefix on a `.js`/`.toml` basename. Test
36
+ * manifests (`*.toml`, `*.list`, `*.ini`) under those directories also
37
+ * qualify.
36
38
  *
37
39
  * @param sourcePath Engine-relative POSIX path
38
40
  * @returns True when the file belongs to the test tree, not the bundle
@@ -41,6 +43,13 @@ export function isTestPath(sourcePath) {
41
43
  if (sourcePath.includes('/test/') || sourcePath.includes('/tests/')) {
42
44
  return true;
43
45
  }
46
+ // `testing/{mochitest,marionette,xpcshell,...}` are test-infrastructure
47
+ // trees that ship under `_tests/`, not `dist/`. Match both as a root
48
+ // segment (e.g. `testing/mochitest/api.js`) and as an interior segment
49
+ // (e.g. a vendored harness under `third_party/.../testing/...`).
50
+ if (sourcePath.startsWith('testing/') || sourcePath.includes('/testing/')) {
51
+ return true;
52
+ }
44
53
  const name = basename(sourcePath);
45
54
  if (/^browser_.+\.(js|toml|ini)$/.test(name))
46
55
  return true;
@@ -19,7 +19,7 @@ export declare const SRC_DIR = "src";
19
19
  /** Supported top-level fireforge.json keys backed by the current schema. */
20
20
  export declare const SUPPORTED_CONFIG_ROOT_KEYS: readonly ["name", "vendor", "appId", "binaryName", "firefox", "build", "license", "wire", "patchLint", "markerComment"];
21
21
  /** Supported config paths that can be read or set without --force. */
22
- export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "build", "build.jobs", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "markerComment"];
22
+ export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "build", "build.jobs", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "markerComment"];
23
23
  /**
24
24
  * Gets all project paths based on a root directory.
25
25
  * @param root - Root directory of the project
@@ -49,6 +49,7 @@ export const SUPPORTED_CONFIG_PATHS = [
49
49
  'patchLint.rawColorAllowlist',
50
50
  'patchLint.jsdocClassMethods',
51
51
  'patchLint.testAssertionFloor',
52
+ 'patchLint.chromeScriptJsDoc',
52
53
  'markerComment',
53
54
  ];
54
55
  /**
@@ -229,6 +229,10 @@ function parsePatchLintBlock(rec) {
229
229
  if (testAssertionFloor !== undefined) {
230
230
  out.testAssertionFloor = testAssertionFloor;
231
231
  }
232
+ const chromeScriptJsDoc = parseSeverityGate(rec.raw('chromeScriptJsDoc'), 'patchLint.chromeScriptJsDoc');
233
+ if (chromeScriptJsDoc !== undefined) {
234
+ out.chromeScriptJsDoc = chromeScriptJsDoc;
235
+ }
232
236
  return out;
233
237
  }
234
238
  //# sourceMappingURL=config-validate.js.map
@@ -29,6 +29,11 @@ export declare function getLicenseHeader(license: ProjectLicense, style: Comment
29
29
  * standard MPL header — operators were forced to `--skip-lint` over a real
30
30
  * false positive.
31
31
  *
32
+ * Editor-directive block comments (`/* -*- ... -*- *\/`, `/* vim: ... *\/`)
33
+ * leading the file are tolerated — Mozilla's canonical layout puts those
34
+ * on lines 1–2 with the MPL header on lines 3+, which the raw
35
+ * `startsWith` check would otherwise miss.
36
+ *
32
37
  * @param content - File content to check
33
38
  * @param style - Comment syntax of the file
34
39
  */
@@ -53,6 +53,39 @@ export function getLicenseHeader(license, style) {
53
53
  return lines.map((l) => `# ${l}`).join('\n');
54
54
  }
55
55
  }
56
+ /**
57
+ * Single-line `/* ... *\/` block comments containing either an Emacs
58
+ * file-mode marker (`-*-`) or a vim modeline (`vim:`) — Mozilla's
59
+ * canonical first-line editor directives that legitimately precede the
60
+ * license header in many Firefox source files.
61
+ *
62
+ * Restricted to single-line blocks so a multi-line license header never
63
+ * gets accidentally consumed.
64
+ */
65
+ const EDITOR_DIRECTIVE_BLOCK_COMMENT = /^[ \t]*\/\*[^\r\n]*?(?:-\*-|\bvim:)[^\r\n]*?\*\/[ \t]*\r?\n?/;
66
+ /**
67
+ * Strips any leading run of editor-directive block comments and blank
68
+ * lines, returning the remaining content.
69
+ *
70
+ * Mozilla's coding convention places editor directives like
71
+ * `/* -*- Mode: javascript; ... -*- *\/` and `/* vim: set ... *\/` on
72
+ * lines 1–2, with the canonical license header following on lines 3+.
73
+ * The raw `content.startsWith(...)` check used by {@link hasAnyLicenseHeader}
74
+ * never matches in that shape; this helper lets the caller test the
75
+ * post-directive prefix as a fallback.
76
+ *
77
+ * @param content - File content to strip
78
+ */
79
+ function stripLeadingEditorDirectives(content) {
80
+ let result = content;
81
+ let prev;
82
+ do {
83
+ prev = result;
84
+ result = result.replace(/^[ \t]*\r?\n/, '');
85
+ result = result.replace(EDITOR_DIRECTIVE_BLOCK_COMMENT, '');
86
+ } while (result !== prev);
87
+ return result;
88
+ }
56
89
  /**
57
90
  * Returns true if `content` starts with any known license header for the
58
91
  * given comment style.
@@ -65,16 +98,24 @@ export function getLicenseHeader(license, style) {
65
98
  * standard MPL header — operators were forced to `--skip-lint` over a real
66
99
  * false positive.
67
100
  *
101
+ * Editor-directive block comments (`/* -*- ... -*- *\/`, `/* vim: ... *\/`)
102
+ * leading the file are tolerated — Mozilla's canonical layout puts those
103
+ * on lines 1–2 with the MPL header on lines 3+, which the raw
104
+ * `startsWith` check would otherwise miss.
105
+ *
68
106
  * @param content - File content to check
69
107
  * @param style - Comment syntax of the file
70
108
  */
71
109
  export function hasAnyLicenseHeader(content, style) {
110
+ const candidates = [content, stripLeadingEditorDirectives(content)];
72
111
  const licenses = Object.keys(HEADER_LINES);
73
- if (licenses.some((license) => content.startsWith(getLicenseHeader(license, style)))) {
74
- return true;
75
- }
76
- if (style === 'js' && content.startsWith(getLicenseHeader('MPL-2.0', 'css'))) {
77
- return true;
112
+ for (const candidate of candidates) {
113
+ if (licenses.some((license) => candidate.startsWith(getLicenseHeader(license, style)))) {
114
+ return true;
115
+ }
116
+ if (style === 'js' && candidate.startsWith(getLicenseHeader('MPL-2.0', 'css'))) {
117
+ return true;
118
+ }
78
119
  }
79
120
  return false;
80
121
  }
@@ -48,3 +48,32 @@ export declare function probeMarionettePort(port?: number): Promise<MarionettePo
48
48
  export declare function assertMarionettePortAvailable(port?: number, options?: {
49
49
  binaryName?: string;
50
50
  }): Promise<void>;
51
+ /**
52
+ * Extracts a `--marionette-port=N` (or `--marionette-port N`) argument from
53
+ * a list of forwarded mach args, if present. Used so an operator passing the
54
+ * port via `--mach-arg --marionette-port=NNNN` gets the same preflight
55
+ * override they would from a first-class `--marionette-port` option, rather
56
+ * than the wrapper probing the default port and refusing.
57
+ *
58
+ * Also recognises `--setpref=marionette.port=NNNN` since that is the path
59
+ * the test command auto-forwards to mach.
60
+ *
61
+ * @param machArgs - Forwarded mach args as they would appear on the command
62
+ * line (one element per token; `--foo=bar` and `--foo bar` both supported).
63
+ * @returns The integer port if a recognised arg is present and parses; else
64
+ * `undefined`.
65
+ */
66
+ export declare function extractForwardedMarionettePort(machArgs: string[]): number | undefined;
67
+ /**
68
+ * Heuristic: do the test paths or forwarded mach args indicate a flavour
69
+ * that actually launches a Marionette-driven browser? Browser-chrome and
70
+ * mochitest do; xpcshell does not. Used to decide whether to auto-forward
71
+ * `--setpref=marionette.port=<n>` to mach when the operator passed
72
+ * `--marionette-port`. A no-paths invocation (the default "run all tests"
73
+ * shape) is treated as marionette-relevant since it includes browser-chrome.
74
+ *
75
+ * @param testPaths - Engine-relative paths after `stripEnginePrefix`.
76
+ * @param machArgs - Forwarded mach args (post-`--mach-arg`).
77
+ * @returns `true` when the run is likely to bind a Marionette listener.
78
+ */
79
+ export declare function isMarionetteFlavor(testPaths: string[], machArgs: string[]): boolean;
@@ -212,4 +212,86 @@ export async function assertMarionettePortAvailable(port = DEFAULT_MARIONETTE_PO
212
212
  throw new GeneralError(`Marionette port ${port} is already in use by ${holder.command} (PID ${holder.pid}). ` +
213
213
  `This is not a FireForge-launched browser; stop the holder process or free the port before rerunning.`);
214
214
  }
215
+ /**
216
+ * Extracts a `--marionette-port=N` (or `--marionette-port N`) argument from
217
+ * a list of forwarded mach args, if present. Used so an operator passing the
218
+ * port via `--mach-arg --marionette-port=NNNN` gets the same preflight
219
+ * override they would from a first-class `--marionette-port` option, rather
220
+ * than the wrapper probing the default port and refusing.
221
+ *
222
+ * Also recognises `--setpref=marionette.port=NNNN` since that is the path
223
+ * the test command auto-forwards to mach.
224
+ *
225
+ * @param machArgs - Forwarded mach args as they would appear on the command
226
+ * line (one element per token; `--foo=bar` and `--foo bar` both supported).
227
+ * @returns The integer port if a recognised arg is present and parses; else
228
+ * `undefined`.
229
+ */
230
+ export function extractForwardedMarionettePort(machArgs) {
231
+ for (let i = 0; i < machArgs.length; i++) {
232
+ const arg = machArgs[i];
233
+ if (arg === undefined)
234
+ continue;
235
+ // `--marionette-port=NNNN`
236
+ let match = /^--marionette-port=(\d+)$/.exec(arg);
237
+ if (match?.[1]) {
238
+ const n = Number.parseInt(match[1], 10);
239
+ if (Number.isFinite(n))
240
+ return n;
241
+ }
242
+ // `--marionette-port NNNN` (two tokens)
243
+ if (arg === '--marionette-port') {
244
+ const next = machArgs[i + 1];
245
+ if (next !== undefined) {
246
+ const n = Number.parseInt(next, 10);
247
+ if (Number.isFinite(n))
248
+ return n;
249
+ }
250
+ }
251
+ // `--setpref=marionette.port=NNNN` — the auto-forward shape; recognised
252
+ // here so a duplicate check at the call site can spot operator-supplied
253
+ // setprefs without re-implementing the parse.
254
+ match = /^--setpref=marionette\.port=(\d+)$/.exec(arg);
255
+ if (match?.[1]) {
256
+ const n = Number.parseInt(match[1], 10);
257
+ if (Number.isFinite(n))
258
+ return n;
259
+ }
260
+ }
261
+ return undefined;
262
+ }
263
+ /**
264
+ * Heuristic: do the test paths or forwarded mach args indicate a flavour
265
+ * that actually launches a Marionette-driven browser? Browser-chrome and
266
+ * mochitest do; xpcshell does not. Used to decide whether to auto-forward
267
+ * `--setpref=marionette.port=<n>` to mach when the operator passed
268
+ * `--marionette-port`. A no-paths invocation (the default "run all tests"
269
+ * shape) is treated as marionette-relevant since it includes browser-chrome.
270
+ *
271
+ * @param testPaths - Engine-relative paths after `stripEnginePrefix`.
272
+ * @param machArgs - Forwarded mach args (post-`--mach-arg`).
273
+ * @returns `true` when the run is likely to bind a Marionette listener.
274
+ */
275
+ export function isMarionetteFlavor(testPaths, machArgs) {
276
+ for (const arg of machArgs) {
277
+ if (/^--flavor=xpcshell\b/.test(arg) || arg === '--flavor=xpcshell-tests')
278
+ return false;
279
+ }
280
+ for (const arg of machArgs) {
281
+ if (/^--flavor=(browser-chrome|mochitest|chrome|a11y)\b/.test(arg))
282
+ return true;
283
+ }
284
+ if (testPaths.length === 0)
285
+ return true;
286
+ for (const path of testPaths) {
287
+ const base = path.split('/').pop() ?? path;
288
+ if (/^browser_.+\.(js|ini|toml)$/.test(base))
289
+ return true;
290
+ if (path.includes('/mochitest/') || path.startsWith('mochitest/'))
291
+ return true;
292
+ if (path.includes('/browser-chrome/') || path.startsWith('browser-chrome/'))
293
+ return true;
294
+ }
295
+ return false;
296
+ }
215
297
  //# sourceMappingURL=marionette-port.js.map
@@ -5,6 +5,16 @@ import type { PatchCategory, PatchesManifest, PatchInfo, PatchMetadata } from '.
5
5
  * @returns Next patch number (e.g., "005" for 4 existing patches)
6
6
  */
7
7
  export declare function getNextPatchNumber(patchesDir: string): Promise<string>;
8
+ /**
9
+ * Sanitizes a human-readable name into a filename slug.
10
+ *
11
+ * Exported so `patch rename` can produce a filename slug from its
12
+ * `--to <new-name>` argument using the exact same convention `export`
13
+ * uses, without duplicating the lowercase + non-alnum collapse + length
14
+ * cap rules. Drift between the two would let an operator rename a patch
15
+ * to a slug `export` could never reach.
16
+ */
17
+ export declare function sanitizeName(name: string): string;
8
18
  /**
9
19
  * Generates the next patch filename with category.
10
20
  * @param patchesDir - Path to the patches directory
@@ -25,9 +25,15 @@ export async function getNextPatchNumber(patchesDir) {
25
25
  return String(nextNumber).padStart(Math.max(3, String(nextNumber).length), '0');
26
26
  }
27
27
  /**
28
- * Sanitizes a string for use in a filename.
28
+ * Sanitizes a human-readable name into a filename slug.
29
+ *
30
+ * Exported so `patch rename` can produce a filename slug from its
31
+ * `--to <new-name>` argument using the exact same convention `export`
32
+ * uses, without duplicating the lowercase + non-alnum collapse + length
33
+ * cap rules. Drift between the two would let an operator rename a patch
34
+ * to a slug `export` could never reach.
29
35
  */
30
- function sanitizeName(name) {
36
+ export function sanitizeName(name) {
31
37
  return name
32
38
  .toLowerCase()
33
39
  .replace(/[^a-z0-9]+/g, '-')
@@ -0,0 +1,47 @@
1
+ /**
2
+ * AST-based JSDoc validation for top-level declarations in patch-owned
3
+ * chrome subscripts (`browser/base/content/<binaryName>*.js` and similar
4
+ * `.js` files loaded via `Services.scriptloader.loadSubScript`).
5
+ *
6
+ * Why a separate module from {@link ./patch-lint-jsdoc.ts}: chrome
7
+ * subscripts are NOT ES modules. They are parsed as scripts (no static
8
+ * `export`) and their top-level `class`/`function` declarations are
9
+ * exposed to the loading window as globals rather than declared exports.
10
+ * The export-walker in `patch-lint-jsdoc.ts` would never visit them.
11
+ *
12
+ * The rule shape — "every top-level class needs a JSDoc, every method
13
+ * needs one when `chromeScriptJsDoc` is at `warning`/`error`, every
14
+ * top-level function needs a matching @param/@returns block" — is
15
+ * identical to the `.sys.mjs` rule once you remove the `export` framing,
16
+ * so the per-declaration validators are reused verbatim from the export
17
+ * module.
18
+ */
19
+ import type { PatchLintIssue } from '../types/commands/index.js';
20
+ import type { PatchLintSeverityGate } from '../types/config.js';
21
+ import type { JsDocIssue, ValidateExportJsDocOptions } from './patch-lint-jsdoc.js';
22
+ /**
23
+ * Validates JSDoc on top-level declarations in a chrome-subscript `.js`
24
+ * source file. A parse failure returns an empty issue list — chrome
25
+ * subscripts that use module-only syntax (rare) silently disable the
26
+ * rule rather than emitting confusing parse-error issues.
27
+ *
28
+ * @param source - File content
29
+ * @param options - Optional gates (e.g. class-method JSDoc severity).
30
+ * `classMethodMode` defaults to `'off'` — the orchestrator passes the
31
+ * `chromeScriptJsDoc` severity here so a single knob controls both
32
+ * class-level and method-level enforcement.
33
+ * @returns Array of JSDoc issues found
34
+ */
35
+ export declare function validateChromeScriptJsDoc(source: string, options?: ValidateExportJsDocOptions): JsDocIssue[];
36
+ /**
37
+ * Per-file dispatch: applies the chrome-subscript JSDoc rule to one file
38
+ * if it qualifies, mapping {@link JsDocIssue} into {@link PatchLintIssue}.
39
+ * Extracted so the orchestrator in `patch-lint.ts` stays under the
40
+ * project's per-file line budget — `patch-lint.ts` invokes this helper
41
+ * inline once per affected JS/MJS file. Returns an empty array when the
42
+ * file does not qualify (not a chrome subscript, not patch-owned, or the
43
+ * mode is `'off'` / unset). The orchestrator pre-computes `isChromeOwned`
44
+ * (true iff the file is a patch-owned `.js` non-`.sys.mjs`) so the call
45
+ * site fits on a single line.
46
+ */
47
+ export declare function lintChromeScriptJsDocForFile(file: string, content: string, isChromeOwned: boolean, mode: PatchLintSeverityGate | undefined): PatchLintIssue[];
@@ -0,0 +1,87 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * AST-based JSDoc validation for top-level declarations in patch-owned
4
+ * chrome subscripts (`browser/base/content/<binaryName>*.js` and similar
5
+ * `.js` files loaded via `Services.scriptloader.loadSubScript`).
6
+ *
7
+ * Why a separate module from {@link ./patch-lint-jsdoc.ts}: chrome
8
+ * subscripts are NOT ES modules. They are parsed as scripts (no static
9
+ * `export`) and their top-level `class`/`function` declarations are
10
+ * exposed to the loading window as globals rather than declared exports.
11
+ * The export-walker in `patch-lint-jsdoc.ts` would never visit them.
12
+ *
13
+ * The rule shape — "every top-level class needs a JSDoc, every method
14
+ * needs one when `chromeScriptJsDoc` is at `warning`/`error`, every
15
+ * top-level function needs a matching @param/@returns block" — is
16
+ * identical to the `.sys.mjs` rule once you remove the `export` framing,
17
+ * so the per-declaration validators are reused verbatim from the export
18
+ * module.
19
+ */
20
+ import { parseScript } from './ast-utils.js';
21
+ import { validateClassDecl, validateClassMethods, validateFunctionDecl, } from './patch-lint-jsdoc.js';
22
+ /**
23
+ * Validates JSDoc on top-level declarations in a chrome-subscript `.js`
24
+ * source file. A parse failure returns an empty issue list — chrome
25
+ * subscripts that use module-only syntax (rare) silently disable the
26
+ * rule rather than emitting confusing parse-error issues.
27
+ *
28
+ * @param source - File content
29
+ * @param options - Optional gates (e.g. class-method JSDoc severity).
30
+ * `classMethodMode` defaults to `'off'` — the orchestrator passes the
31
+ * `chromeScriptJsDoc` severity here so a single knob controls both
32
+ * class-level and method-level enforcement.
33
+ * @returns Array of JSDoc issues found
34
+ */
35
+ export function validateChromeScriptJsDoc(source, options) {
36
+ const classMethodMode = options?.classMethodMode ?? 'off';
37
+ const comments = [];
38
+ let ast;
39
+ try {
40
+ ast = parseScript(source, comments);
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ const issues = [];
46
+ const body = ast.body;
47
+ for (const node of body) {
48
+ if (node.type === 'FunctionDeclaration') {
49
+ validateFunctionDecl(node, comments, source, issues);
50
+ }
51
+ else if (node.type === 'ClassDeclaration') {
52
+ validateClassDecl(node, comments, source, issues);
53
+ if (classMethodMode !== 'off') {
54
+ validateClassMethods(node, comments, source, issues, classMethodMode);
55
+ }
56
+ }
57
+ // Top-level `var Foo = class {...}` / `let Foo = function() {...}`
58
+ // patterns are intentionally out of scope for V1 — chrome subscripts
59
+ // overwhelmingly use bare `class`/`function` declarations and the
60
+ // variable-init form would require unwrapping the initializer. Add
61
+ // it later if a real chrome subscript uses that shape.
62
+ }
63
+ return issues;
64
+ }
65
+ /**
66
+ * Per-file dispatch: applies the chrome-subscript JSDoc rule to one file
67
+ * if it qualifies, mapping {@link JsDocIssue} into {@link PatchLintIssue}.
68
+ * Extracted so the orchestrator in `patch-lint.ts` stays under the
69
+ * project's per-file line budget — `patch-lint.ts` invokes this helper
70
+ * inline once per affected JS/MJS file. Returns an empty array when the
71
+ * file does not qualify (not a chrome subscript, not patch-owned, or the
72
+ * mode is `'off'` / unset). The orchestrator pre-computes `isChromeOwned`
73
+ * (true iff the file is a patch-owned `.js` non-`.sys.mjs`) so the call
74
+ * site fits on a single line.
75
+ */
76
+ export function lintChromeScriptJsDocForFile(file, content, isChromeOwned, mode) {
77
+ if (!isChromeOwned || !mode || mode === 'off')
78
+ return [];
79
+ const jsdocIssues = validateChromeScriptJsDoc(content, { classMethodMode: mode });
80
+ return jsdocIssues.map((issue) => ({
81
+ file,
82
+ check: issue.check,
83
+ message: issue.message,
84
+ severity: issue.severity ?? mode,
85
+ }));
86
+ }
87
+ //# sourceMappingURL=patch-lint-chrome-jsdoc.js.map
@@ -396,6 +396,10 @@ export function lintPatchQueueForwardImports(ctx) {
396
396
  .map((o) => `${o.filename}:${o.fullPath}`)
397
397
  .sort((a, b) => a.localeCompare(b))
398
398
  .join(',');
399
+ // Lowest ordinal that lands AFTER every later-ordered creator —
400
+ // turns the operator's "guess and re-run" loop into a single shot
401
+ // when the only fix is reordering.
402
+ const suggestedOrder = Math.max(...laterOwners.map((o) => o.order)) + 1;
399
403
  issues.push({
400
404
  file: sitePath,
401
405
  check: 'forward-import',
@@ -404,7 +408,8 @@ export function lintPatchQueueForwardImports(ctx) {
404
408
  `but the matching new file is created by a later patch: ${ownersSummary}. ` +
405
409
  'Reorder the patches so the dependency is created first, move the import ' +
406
410
  'into the later patch, or mark the import with ' +
407
- `"// ${FORWARD_IMPORT_IGNORE_MARKER}" if the basename collision is a false positive.`,
411
+ `"// ${FORWARD_IMPORT_IGNORE_MARKER}" if the basename collision is a false positive. ` +
412
+ `Closest legal ordinal that satisfies this dependency: ${suggestedOrder}.`,
408
413
  severity: 'error',
409
414
  });
410
415
  }
@@ -6,6 +6,9 @@
6
6
  * Separated from `patch-lint.ts` to keep both files within the
7
7
  * project's per-file line budget.
8
8
  */
9
+ import type * as acorn from 'acorn';
10
+ import type { ClassDeclaration, FunctionDeclaration } from 'estree';
11
+ import type { AcornESTreeNode } from './ast-utils.js';
9
12
  export type JsDocCheck = 'missing-jsdoc' | 'jsdoc-param-mismatch' | 'jsdoc-missing-returns' | 'missing-jsdoc-class-method' | 'jsdoc-class-method-param-mismatch' | 'jsdoc-class-method-missing-returns';
10
13
  export interface JsDocIssue {
11
14
  line: number;
@@ -19,6 +22,40 @@ export interface ValidateExportJsDocOptions {
19
22
  /** Gate for class-method JSDoc enforcement. Default 'off' (no walking). */
20
23
  classMethodMode?: ClassMethodMode;
21
24
  }
25
+ /**
26
+ * Validates a top-level function declaration: requires an attached JSDoc
27
+ * comment, then checks @param name matching and @returns presence. Exported
28
+ * so the chrome-subscript validator (`patch-lint-chrome-jsdoc.ts`) can reuse
29
+ * it on `parseScript`-produced declarations — the rule shape is identical
30
+ * between ES-module exports and chrome-subscript top-level declarations.
31
+ *
32
+ * @param fn - FunctionDeclaration AST node
33
+ * @param comments - All Acorn comments collected from the source
34
+ * @param source - Original source text (for line-number resolution)
35
+ * @param issues - Output sink for JSDoc issues
36
+ * @param lookupStart - Optional offset to use when locating the attached
37
+ * JSDoc (defaults to `fn.start`). Used by the export-walker so the JSDoc
38
+ * is found relative to the `export` keyword rather than the inner decl.
39
+ */
40
+ export declare function validateFunctionDecl(fn: AcornESTreeNode<FunctionDeclaration>, comments: acorn.Comment[], source: string, issues: JsDocIssue[], lookupStart?: number): void;
41
+ /**
42
+ * Validates a top-level class declaration: requires an attached JSDoc
43
+ * comment on the class itself. Method-level checks live in
44
+ * {@link validateClassMethods}. Exported for reuse in the chrome-subscript
45
+ * validator.
46
+ */
47
+ export declare function validateClassDecl(cls: AcornESTreeNode<ClassDeclaration>, comments: acorn.Comment[], source: string, issues: JsDocIssue[], lookupStart?: number): void;
48
+ /**
49
+ * Walks an exported class body and emits class-method JSDoc issues per
50
+ * the configured severity. Skip rules (in evaluation order):
51
+ * 1. private syntax (`#foo`) and underscore-prefixed names
52
+ * 2. zero-parameter constructors
53
+ * 3. methods whose JSDoc carries `@private` or `@internal`
54
+ *
55
+ * Pure-override skip (`super.method(...args)`-only bodies bypassing the
56
+ * @returns check) is deferred — V1 keeps the rule simple.
57
+ */
58
+ export declare function validateClassMethods(cls: AcornESTreeNode<ClassDeclaration>, comments: acorn.Comment[], source: string, issues: JsDocIssue[], severity: 'warning' | 'error'): void;
22
59
  /**
23
60
  * Validates JSDoc on exported declarations in a `.sys.mjs` source file.
24
61
  *
@@ -166,7 +166,22 @@ function validateParamsAndReturns(fnNode, jsDoc, issues, ctx) {
166
166
  // ---------------------------------------------------------------------------
167
167
  // Top-level export validation
168
168
  // ---------------------------------------------------------------------------
169
- function validateFunctionDecl(fn, comments, source, issues, lookupStart) {
169
+ /**
170
+ * Validates a top-level function declaration: requires an attached JSDoc
171
+ * comment, then checks @param name matching and @returns presence. Exported
172
+ * so the chrome-subscript validator (`patch-lint-chrome-jsdoc.ts`) can reuse
173
+ * it on `parseScript`-produced declarations — the rule shape is identical
174
+ * between ES-module exports and chrome-subscript top-level declarations.
175
+ *
176
+ * @param fn - FunctionDeclaration AST node
177
+ * @param comments - All Acorn comments collected from the source
178
+ * @param source - Original source text (for line-number resolution)
179
+ * @param issues - Output sink for JSDoc issues
180
+ * @param lookupStart - Optional offset to use when locating the attached
181
+ * JSDoc (defaults to `fn.start`). Used by the export-walker so the JSDoc
182
+ * is found relative to the `export` keyword rather than the inner decl.
183
+ */
184
+ export function validateFunctionDecl(fn, comments, source, issues, lookupStart) {
170
185
  const name = fn.id.name;
171
186
  const start = lookupStart !== undefined ? lookupStart : fn.start;
172
187
  const line = lineAt(source, start);
@@ -186,7 +201,13 @@ function validateFunctionDecl(fn, comments, source, issues, lookupStart) {
186
201
  returnsCheck: 'jsdoc-missing-returns',
187
202
  });
188
203
  }
189
- function validateClassDecl(cls, comments, source, issues, lookupStart) {
204
+ /**
205
+ * Validates a top-level class declaration: requires an attached JSDoc
206
+ * comment on the class itself. Method-level checks live in
207
+ * {@link validateClassMethods}. Exported for reuse in the chrome-subscript
208
+ * validator.
209
+ */
210
+ export function validateClassDecl(cls, comments, source, issues, lookupStart) {
190
211
  const name = cls.id.name;
191
212
  const start = lookupStart !== undefined ? lookupStart : cls.start;
192
213
  const line = lineAt(source, start);
@@ -254,7 +275,7 @@ function classMethodLabel(className, method, name) {
254
275
  * Pure-override skip (`super.method(...args)`-only bodies bypassing the
255
276
  * @returns check) is deferred — V1 keeps the rule simple.
256
277
  */
257
- function validateClassMethods(cls, comments, source, issues, severity) {
278
+ export function validateClassMethods(cls, comments, source, issues, severity) {
258
279
  const className = cls.id.name;
259
280
  for (const member of cls.body.body) {
260
281
  if (member.type !== 'MethodDefinition')
@@ -1,10 +1,17 @@
1
1
  /**
2
- * Patch ownership resolution for `.sys.mjs` files.
2
+ * Patch ownership resolution for JS-shaped files.
3
3
  *
4
4
  * A file is "patch-owned" when it was created by the project's patch
5
5
  * queue rather than being an upstream Firefox file that happens to be
6
- * modified. This module computes the set of patch-owned `.sys.mjs`
7
- * paths so lint rules can scope enforcement to project code only.
6
+ * modified. This module computes the set of patch-owned paths so lint
7
+ * rules can scope enforcement to project code only.
8
+ *
9
+ * The two resolvers are kept separate (one per extension predicate)
10
+ * because the downstream rules differ: `.sys.mjs` files go through
11
+ * `runCheckJs` (TypeScript checkJs) and the export-walker JSDoc rule;
12
+ * chrome subscripts (`.js` non-`.sys.mjs`) only get the script-walker
13
+ * JSDoc rule. Mixing them in a single set would silently broaden
14
+ * `runCheckJs` to chrome subscripts, which it is not designed for.
8
15
  */
9
16
  import type { PatchQueueContext } from './patch-lint-cross.js';
10
17
  /**
@@ -23,3 +30,14 @@ import type { PatchQueueContext } from './patch-lint-cross.js';
23
30
  * @returns Set of patch-owned `.sys.mjs` file paths
24
31
  */
25
32
  export declare function resolvePatchOwnedSysMjs(currentNewFiles: Set<string>, patchQueueCtx?: PatchQueueContext): Set<string>;
33
+ /**
34
+ * Returns the set of file paths that are patch-owned chrome subscripts
35
+ * (`.js` files that are not `.sys.mjs` modules — typically
36
+ * `browser/base/content/<binaryName>*.js` and similar). Same ownership
37
+ * semantics as {@link resolvePatchOwnedSysMjs}.
38
+ *
39
+ * @param currentNewFiles - Files newly created in the current diff
40
+ * @param patchQueueCtx - Optional cross-patch context for queue-wide ownership
41
+ * @returns Set of patch-owned chrome-subscript file paths
42
+ */
43
+ export declare function resolvePatchOwnedChromeScripts(currentNewFiles: Set<string>, patchQueueCtx?: PatchQueueContext): Set<string>;