@hominis/fireforge 0.18.6 → 0.18.9
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/README.md +26 -18
- package/dist/src/commands/test.js +50 -3
- package/dist/src/core/ast-utils.d.ts +5 -1
- package/dist/src/core/ast-utils.js +10 -3
- package/dist/src/core/build-audit-resolve.d.ts +5 -3
- package/dist/src/core/build-audit-resolve.js +12 -3
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +3 -0
- package/dist/src/core/config-validate.js +41 -16
- package/dist/src/core/marionette-port.d.ts +29 -0
- package/dist/src/core/marionette-port.js +82 -0
- package/dist/src/core/patch-lint-chrome-jsdoc.d.ts +47 -0
- package/dist/src/core/patch-lint-chrome-jsdoc.js +87 -0
- package/dist/src/core/patch-lint-jsdoc.d.ts +47 -2
- package/dist/src/core/patch-lint-jsdoc.js +162 -27
- package/dist/src/core/patch-lint-ownership.d.ts +21 -3
- package/dist/src/core/patch-lint-ownership.js +45 -18
- package/dist/src/core/patch-lint.d.ts +7 -2
- package/dist/src/core/patch-lint.js +54 -6
- package/dist/src/types/commands/options.d.ts +8 -0
- package/dist/src/types/config.d.ts +20 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -230,29 +230,37 @@ If the manifest drifts after an interrupted export or manual edits, `fireforge i
|
|
|
230
230
|
|
|
231
231
|
By default, a standalone `fireforge lint` (no arguments) lints the **aggregate** `git diff HEAD` — i.e. every applied patch summed — with tool-managed branding paths (`browser/branding/<binaryName>/`) excluded. A fresh-setup workspace carries a large generated branding diff that operators did not author directly, and letting it through tripped the patch-size and license-header rules on content that matches the `branding` bucket in `fireforge status`. When the exclusion fires the command prints a one-line note naming the excluded count so the filter is visible. On a repo where `fireforge import` or `fireforge rebase` has just applied the full queue, the patch-size rules (`large-patch-lines`, `large-patch-files`) fire against the sum, which reads as "my queue is broken" when it is really an artefact of aggregation. Use `fireforge lint --per-patch` to rescope the diff to each patch's own `filesAffected`, honouring the patch's own `lintIgnore`. Cross-patch rules (`duplicate-new-file-creation`, `forward-import`) still run once over the whole queue either way. Pass explicit file paths to narrow the scope further — explicit-path mode does lint branding files (the operator's explicit request wins over the branding exclusion); the three modes (aggregate, file-scoped, per-patch) are mutually exclusive.
|
|
232
232
|
|
|
233
|
-
| Check
|
|
234
|
-
|
|
|
235
|
-
| `missing-license-header`
|
|
236
|
-
| `relative-import`
|
|
237
|
-
| `token-prefix-violation`
|
|
238
|
-
| `raw-color-value`
|
|
239
|
-
| `duplicate-new-file-creation`
|
|
240
|
-
| `forward-import`
|
|
241
|
-
| `missing-jsdoc`
|
|
242
|
-
| `jsdoc-param-mismatch`
|
|
243
|
-
| `jsdoc-missing-returns`
|
|
244
|
-
| `checkjs-type-error`
|
|
245
|
-
| `missing-
|
|
246
|
-
| `
|
|
247
|
-
| `
|
|
248
|
-
| `
|
|
249
|
-
| `
|
|
250
|
-
| `
|
|
233
|
+
| Check | Scope | Severity |
|
|
234
|
+
| ------------------------------------ | ------------------------------------------------------------------------------------------------ | ------------------------ |
|
|
235
|
+
| `missing-license-header` | New files (JS/CSS/FTL) | error |
|
|
236
|
+
| `relative-import` | JS/MJS files | error |
|
|
237
|
+
| `token-prefix-violation` | CSS files (with furnace) | error |
|
|
238
|
+
| `raw-color-value` | Introduced CSS color values (allowlist via `patchLint.rawColorAllowlist`) | error |
|
|
239
|
+
| `duplicate-new-file-creation` | Same path created by multiple patches | error |
|
|
240
|
+
| `forward-import` | Patch imports from a later-patch file | error |
|
|
241
|
+
| `missing-jsdoc` | Exports in patch-owned `.sys.mjs` | error |
|
|
242
|
+
| `jsdoc-param-mismatch` | Exports in patch-owned `.sys.mjs` | error |
|
|
243
|
+
| `jsdoc-missing-returns` | Exports in patch-owned `.sys.mjs` | error |
|
|
244
|
+
| `checkjs-type-error` | Patch-owned `.sys.mjs` (opt-in) | error |
|
|
245
|
+
| `missing-jsdoc-class-method` | Class-method exports in patch-owned `.sys.mjs` (opt-in) | configurable |
|
|
246
|
+
| `jsdoc-class-method-param-mismatch` | Class-method exports in patch-owned `.sys.mjs` (opt-in) | configurable |
|
|
247
|
+
| `jsdoc-class-method-missing-returns` | Class-method exports in patch-owned `.sys.mjs` (opt-in) | configurable |
|
|
248
|
+
| `test-needs-assertion` | Patch-introduced `browser_*.js` test files (opt-in) | configurable |
|
|
249
|
+
| `missing-modification-comment` | Modified upstream JS/MJS | warning |
|
|
250
|
+
| `modified-file-missing-header` | Modified upstream files (JS/CSS/FTL) | warning |
|
|
251
|
+
| `file-too-large` | New files (tiered: 500/750/900 general, 1200/1400/1600 test) | notice / warning / error |
|
|
252
|
+
| `observer-topic-naming` | Observer topics with binaryName | warning |
|
|
253
|
+
| `large-patch-files` | Patches affecting many files (tiered: >5 general, >5 test, >60 branding) | warning |
|
|
254
|
+
| `large-patch-lines` | Patch line count (tiered: 800/1500/3000 general, 1500/3000/6000 test, 8000/18000/30000 branding) | notice / warning / error |
|
|
251
255
|
|
|
252
256
|
**JSDoc validation** uses AST-based analysis (Acorn) to validate exported APIs in patch-owned `.sys.mjs` files. A file is "patch-owned" if it was newly created by the current diff or by an existing patch in the queue. Functions must document every `@param` (names must match) and include `@returns` when the function returns a value. Exported constants and classes require a JSDoc block.
|
|
253
257
|
|
|
254
258
|
**Optional `checkJs` pass.** Enable a TypeScript-esque bastardization of type checking for patch-owned `.sys.mjs` files by adding `"patchLint": { "checkJs": true }` to `fireforge.json`. This uses the TypeScript compiler API with `allowJs + checkJs + noEmit`, scoped only to patch-owned files. Firefox globals (`Services`, `ChromeUtils`, `lazy`, etc.) are shimmed automatically. Module-resolution errors from Firefox's `resource://` and `chrome://` URL schemes are suppressed since TypeScript cannot follow these. This pass solely focuses on type errors within the patch-owned code itself (mismatched JSDoc types, wrong argument counts, unreachable code, etc.).
|
|
255
259
|
|
|
260
|
+
**Optional `jsdocClassMethods` enforcement.** Set `"patchLint": { "jsdocClassMethods": "warning" | "error" }` in `fireforge.json` to extend JSDoc validation to class-method exports inside patch-owned `.sys.mjs` files. Every public method (instance and static), parameter-bearing constructor, getter, and setter must carry a leading JSDoc block; `@param` names must match the parameter list, and `@returns` is required when a method returns a value (getters and setters are exempt from `@returns`). Methods whose name starts with `_` or `#`, methods carrying `@private` or `@internal` in their JSDoc, and zero-parameter constructors are exempt. Defaults to `"off"`, so upgrading is a no-op until the knob is set.
|
|
261
|
+
|
|
262
|
+
**Optional `testAssertionFloor` enforcement.** Set `"patchLint": { "testAssertionFloor": "warning" | "error" }` to require that every `browser_*.js` test file introduced by the current patch contains at least one assertion (`Assert.*`, `ok()`, `is()`, `isnot()`, or `isDeeply()`). Smoke-only tests that load the script and exit without asserting any user-visible behavior are flagged as `test-needs-assertion`. Comment-only assertions do not count — comments are stripped before scanning. `head.js` and `head_*.js` test helpers are exempt; modified upstream tests are out of scope (V1 only flags newly-introduced files). Defaults to `"off"`.
|
|
263
|
+
|
|
256
264
|
The two cross-patch rules (`duplicate-new-file-creation` and `forward-import`) run over the whole patch queue rather than a single diff, catching ordering issues that only surface during `import`. Forward-import detection compares leaf filenames, so a false positive is theoretically possible when two patches create files with the same basename in different directories. Suppress with an inline `// fireforge-ignore: forward-import` comment on or above the import line. Both `forward-import` and `raw-color-value` support inline suppression comments (`// fireforge-ignore: forward-import` and `/* fireforge-ignore: raw-color-value */` respectively).
|
|
257
265
|
|
|
258
266
|
</details>
|
|
@@ -3,7 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { prepareBuildEnvironment } from '../core/build-prepare.js';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
5
5
|
import { buildArtifactMismatchMessage, buildUI, hasBuildArtifacts, hasRunnableBundle, testWithOutput, } from '../core/mach.js';
|
|
6
|
-
import { assertMarionettePortAvailable } from '../core/marionette-port.js';
|
|
6
|
+
import { assertMarionettePortAvailable, extractForwardedMarionettePort, isMarionetteFlavor, } from '../core/marionette-port.js';
|
|
7
7
|
import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionettePreflight, } from '../core/marionette-preflight.js';
|
|
8
8
|
import { checkStaleBuildForTest, formatStaleBuildWarning } from '../core/test-stale-check.js';
|
|
9
9
|
import { operatorAlreadySetAppPath, resolveXpcshellAppdirArg, } from '../core/xpcshell-appdir.js';
|
|
@@ -238,6 +238,20 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
238
238
|
warn(formatStaleBuildWarning(stale));
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
|
+
// Resolve the effective Marionette port. Operator precedence:
|
|
242
|
+
// 1. `--marionette-port` (first-class option, parsed at the CLI layer)
|
|
243
|
+
// 2. forwarded `--mach-arg --marionette-port=NNNN` /
|
|
244
|
+
// `--mach-arg --setpref=marionette.port=NNNN`
|
|
245
|
+
// 3. fall back to `DEFAULT_MARIONETTE_PORT` semantics inside the probes
|
|
246
|
+
// (passed as `undefined`).
|
|
247
|
+
// Without (2), an operator working around a stale listener via the
|
|
248
|
+
// documented `--mach-arg --marionette-port=NNNN` workaround would still
|
|
249
|
+
// hit the wrapper preflight refusing on 2828 before the forwarded arg
|
|
250
|
+
// ever reached mach.
|
|
251
|
+
const forwardedPort = options.machArg
|
|
252
|
+
? extractForwardedMarionettePort(options.machArg)
|
|
253
|
+
: undefined;
|
|
254
|
+
const effectivePort = options.marionettePort ?? forwardedPort;
|
|
241
255
|
// Stale-browser probe: an interrupted earlier test run can leave a
|
|
242
256
|
// Firefox/ForgeFresh/Hominis instance listening on the Marionette
|
|
243
257
|
// control port, which breaks the next mach test launch with a
|
|
@@ -246,7 +260,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
246
260
|
// generic bind failure. 2026-04-21 eval (Finding #20): a stale
|
|
247
261
|
// `-marionette` process from `fresh/` poisoned a later test run in
|
|
248
262
|
// the sibling `hominis/` workspace.
|
|
249
|
-
await assertMarionettePortAvailable(
|
|
263
|
+
await assertMarionettePortAvailable(effectivePort, { binaryName: projectConfig.binaryName });
|
|
250
264
|
// `--doctor` runs a short marionette handshake probe. When test paths are
|
|
251
265
|
// supplied the probe gates the mach test invocation (a FAIL bails out). When
|
|
252
266
|
// no paths are supplied this is the only step — it's the fastest way to tell
|
|
@@ -259,7 +273,9 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
259
273
|
// clack box-drawing framing.
|
|
260
274
|
process.stdout.write('Running marionette preflight...\n');
|
|
261
275
|
info('Running marionette preflight...');
|
|
262
|
-
const preflight =
|
|
276
|
+
const preflight = effectivePort !== undefined
|
|
277
|
+
? await runMarionettePreflight(paths.engine, { port: effectivePort })
|
|
278
|
+
: await runMarionettePreflight(paths.engine);
|
|
263
279
|
// 2026-04-24 eval Finding 7: the pre-0.18.1 code used
|
|
264
280
|
// `success()` + `outro()` + a direct `process.stdout.write` as a
|
|
265
281
|
// belt-and-suspenders but still reproducibly dropped the PASS summary
|
|
@@ -303,6 +319,30 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
303
319
|
if (options.machArg && options.machArg.length > 0) {
|
|
304
320
|
extraArgs.push(...options.machArg);
|
|
305
321
|
}
|
|
322
|
+
// Auto-forward the Marionette port to mach when `--marionette-port` is
|
|
323
|
+
// set. We use `--setpref=marionette.port=<n>` because the marionette
|
|
324
|
+
// listener reads that pref before binding (browser-chrome / mochitest
|
|
325
|
+
// path); xpcshell never reads it, so the pref is a no-op there.
|
|
326
|
+
//
|
|
327
|
+
// Skip forwarding when the operator already supplied an equivalent arg
|
|
328
|
+
// via `--mach-arg` — duplicates would be confusing without changing
|
|
329
|
+
// semantics. Skip with a notice for clearly-non-marionette flavours
|
|
330
|
+
// (xpcshell, or paths that don't look browser-chrome/mochitest) so the
|
|
331
|
+
// operator knows the preflight took the override but mach was not
|
|
332
|
+
// auto-configured. Same escape valve applies: any mach arg can still
|
|
333
|
+
// be supplied via `--mach-arg`.
|
|
334
|
+
if (options.marionettePort !== undefined) {
|
|
335
|
+
const operatorAlreadyForwarded = forwardedPort !== undefined;
|
|
336
|
+
if (operatorAlreadyForwarded) {
|
|
337
|
+
info(`--marionette-port=${options.marionettePort} set, but the same port is already forwarded via --mach-arg; skipping auto-forward.`);
|
|
338
|
+
}
|
|
339
|
+
else if (isMarionetteFlavor(normalizedPaths, options.machArg ?? [])) {
|
|
340
|
+
extraArgs.push(`--setpref=marionette.port=${options.marionettePort}`);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
info(`--marionette-port=${options.marionettePort} applied to the preflight probe, but the test paths do not look browser-chrome/mochitest — mach is not auto-configured. Pass --mach-arg --setpref=marionette.port=${options.marionettePort} explicitly if mach should also use this port.`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
306
346
|
// xpcshell appdir auto-injection — see src/core/xpcshell-appdir.ts for the
|
|
307
347
|
// full motivation. On rebranded forks (appname != "firefox") the upstream
|
|
308
348
|
// harness silently ignores `firefox-appdir = "browser"` directives in the
|
|
@@ -368,6 +408,13 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
|
|
|
368
408
|
acc.push(value);
|
|
369
409
|
return acc;
|
|
370
410
|
}, [])
|
|
411
|
+
.option('--marionette-port <port>', 'Override the Marionette control port (default 2828) for the stale-browser probe, the --doctor preflight, and the auto-forwarded --setpref=marionette.port=<n> arg passed to mach. Use this when a stale process holds 2828 or a CI runner reserves a different port.', (raw) => {
|
|
412
|
+
const n = Number.parseInt(raw, 10);
|
|
413
|
+
if (!Number.isFinite(n) || n < 1 || n > 65535) {
|
|
414
|
+
throw new GeneralError(`--marionette-port must be an integer in 1..65535 (got "${raw}")`);
|
|
415
|
+
}
|
|
416
|
+
return n;
|
|
417
|
+
})
|
|
371
418
|
.action(withErrorHandling(async (paths, options) => {
|
|
372
419
|
await testCommand(getProjectRoot(), paths, pickDefined(options));
|
|
373
420
|
}));
|
|
@@ -15,8 +15,12 @@ export type AcornESTreeNode<T extends estree.Node = estree.Node> = T & {
|
|
|
15
15
|
* Parse JavaScript source as a **script** (not an ES module).
|
|
16
16
|
* All Mozilla chrome JS files (`browser-main.js`, `browser-init.js`,
|
|
17
17
|
* `customElements.js`, etc.) are scripts that run in a privileged scope.
|
|
18
|
+
*
|
|
19
|
+
* @param content - Source text to parse
|
|
20
|
+
* @param onComment - Optional array that acorn fills with comment nodes
|
|
21
|
+
* @returns Parsed program AST with character-offset positions
|
|
18
22
|
*/
|
|
19
|
-
export declare function parseScript(content: string): AcornESTreeNode<estree.Program>;
|
|
23
|
+
export declare function parseScript(content: string, onComment?: acorn.Comment[]): AcornESTreeNode<estree.Program>;
|
|
20
24
|
/**
|
|
21
25
|
* Parse JavaScript source as an **ES module**.
|
|
22
26
|
* Used for `.sys.mjs` files which use static import/export syntax.
|
|
@@ -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
|
-
|
|
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,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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,
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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", "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
|
|
@@ -129,22 +129,7 @@ export function validateConfig(data) {
|
|
|
129
129
|
// PatchLint
|
|
130
130
|
const patchLintRec = optionalConfigObject(rec, 'patchLint');
|
|
131
131
|
if (patchLintRec) {
|
|
132
|
-
config.patchLint =
|
|
133
|
-
const checkJs = patchLintRec.raw('checkJs');
|
|
134
|
-
if (checkJs !== undefined) {
|
|
135
|
-
if (typeof checkJs !== 'boolean') {
|
|
136
|
-
throw new ConfigError('Config field "patchLint.checkJs" must be a boolean');
|
|
137
|
-
}
|
|
138
|
-
config.patchLint.checkJs = checkJs;
|
|
139
|
-
}
|
|
140
|
-
const rawColorAllowlist = patchLintRec.raw('rawColorAllowlist');
|
|
141
|
-
if (rawColorAllowlist !== undefined) {
|
|
142
|
-
if (!Array.isArray(rawColorAllowlist) ||
|
|
143
|
-
rawColorAllowlist.some((v) => typeof v !== 'string')) {
|
|
144
|
-
throw new ConfigError('Config field "patchLint.rawColorAllowlist" must be an array of strings');
|
|
145
|
-
}
|
|
146
|
-
config.patchLint.rawColorAllowlist = rawColorAllowlist;
|
|
147
|
-
}
|
|
132
|
+
config.patchLint = parsePatchLintBlock(patchLintRec);
|
|
148
133
|
}
|
|
149
134
|
// Warn on unknown root keys
|
|
150
135
|
const knownRootKeys = new Set(SUPPORTED_CONFIG_ROOT_KEYS);
|
|
@@ -210,4 +195,44 @@ function optionalConfigObject(rec, key) {
|
|
|
210
195
|
throw new ConfigError(`Config field "${key}" must be an object`);
|
|
211
196
|
}
|
|
212
197
|
}
|
|
198
|
+
const SEVERITY_GATE_VALUES = ['off', 'warning', 'error'];
|
|
199
|
+
function parseSeverityGate(raw, label) {
|
|
200
|
+
if (raw === undefined)
|
|
201
|
+
return undefined;
|
|
202
|
+
if (typeof raw !== 'string' || !SEVERITY_GATE_VALUES.includes(raw)) {
|
|
203
|
+
throw new ConfigError(`Config field "${label}" must be one of: ${SEVERITY_GATE_VALUES.join(', ')}`);
|
|
204
|
+
}
|
|
205
|
+
return raw;
|
|
206
|
+
}
|
|
207
|
+
function parsePatchLintBlock(rec) {
|
|
208
|
+
const out = {};
|
|
209
|
+
const checkJs = rec.raw('checkJs');
|
|
210
|
+
if (checkJs !== undefined) {
|
|
211
|
+
if (typeof checkJs !== 'boolean') {
|
|
212
|
+
throw new ConfigError('Config field "patchLint.checkJs" must be a boolean');
|
|
213
|
+
}
|
|
214
|
+
out.checkJs = checkJs;
|
|
215
|
+
}
|
|
216
|
+
const rawColorAllowlist = rec.raw('rawColorAllowlist');
|
|
217
|
+
if (rawColorAllowlist !== undefined) {
|
|
218
|
+
if (!Array.isArray(rawColorAllowlist) ||
|
|
219
|
+
rawColorAllowlist.some((v) => typeof v !== 'string')) {
|
|
220
|
+
throw new ConfigError('Config field "patchLint.rawColorAllowlist" must be an array of strings');
|
|
221
|
+
}
|
|
222
|
+
out.rawColorAllowlist = rawColorAllowlist;
|
|
223
|
+
}
|
|
224
|
+
const jsdocClassMethods = parseSeverityGate(rec.raw('jsdocClassMethods'), 'patchLint.jsdocClassMethods');
|
|
225
|
+
if (jsdocClassMethods !== undefined) {
|
|
226
|
+
out.jsdocClassMethods = jsdocClassMethods;
|
|
227
|
+
}
|
|
228
|
+
const testAssertionFloor = parseSeverityGate(rec.raw('testAssertionFloor'), 'patchLint.testAssertionFloor');
|
|
229
|
+
if (testAssertionFloor !== undefined) {
|
|
230
|
+
out.testAssertionFloor = testAssertionFloor;
|
|
231
|
+
}
|
|
232
|
+
const chromeScriptJsDoc = parseSeverityGate(rec.raw('chromeScriptJsDoc'), 'patchLint.chromeScriptJsDoc');
|
|
233
|
+
if (chromeScriptJsDoc !== undefined) {
|
|
234
|
+
out.chromeScriptJsDoc = chromeScriptJsDoc;
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
213
238
|
//# sourceMappingURL=config-validate.js.map
|
|
@@ -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
|
|
@@ -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
|
|
@@ -6,16 +6,61 @@
|
|
|
6
6
|
* Separated from `patch-lint.ts` to keep both files within the
|
|
7
7
|
* project's per-file line budget.
|
|
8
8
|
*/
|
|
9
|
-
|
|
9
|
+
import type * as acorn from 'acorn';
|
|
10
|
+
import type { ClassDeclaration, FunctionDeclaration } from 'estree';
|
|
11
|
+
import type { AcornESTreeNode } from './ast-utils.js';
|
|
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;
|
|
12
15
|
check: JsDocCheck;
|
|
13
16
|
message: string;
|
|
17
|
+
/** Optional severity hint. When undefined, callers default to 'error'. */
|
|
18
|
+
severity?: 'error' | 'warning';
|
|
14
19
|
}
|
|
20
|
+
export type ClassMethodMode = 'off' | 'warning' | 'error';
|
|
21
|
+
export interface ValidateExportJsDocOptions {
|
|
22
|
+
/** Gate for class-method JSDoc enforcement. Default 'off' (no walking). */
|
|
23
|
+
classMethodMode?: ClassMethodMode;
|
|
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;
|
|
15
59
|
/**
|
|
16
60
|
* Validates JSDoc on exported declarations in a `.sys.mjs` source file.
|
|
17
61
|
*
|
|
18
62
|
* @param source - File content
|
|
63
|
+
* @param options - Optional gates for opt-in checks (e.g. class-method JSDoc)
|
|
19
64
|
* @returns Array of JSDoc issues found
|
|
20
65
|
*/
|
|
21
|
-
export declare function validateExportJsDoc(source: string): JsDocIssue[];
|
|
66
|
+
export declare function validateExportJsDoc(source: string, options?: ValidateExportJsDocOptions): JsDocIssue[];
|
|
@@ -50,6 +50,9 @@ function extractParamNames(jsDoc) {
|
|
|
50
50
|
function hasReturnsTag(jsDoc) {
|
|
51
51
|
return /@returns?\b/.test(jsDoc);
|
|
52
52
|
}
|
|
53
|
+
function hasPrivateOrInternalTag(jsDoc) {
|
|
54
|
+
return /@(?:private|internal)\b/.test(jsDoc);
|
|
55
|
+
}
|
|
53
56
|
// ---------------------------------------------------------------------------
|
|
54
57
|
// Return-statement detection
|
|
55
58
|
// ---------------------------------------------------------------------------
|
|
@@ -125,10 +128,60 @@ function lineAt(source, offset) {
|
|
|
125
128
|
}
|
|
126
129
|
return line;
|
|
127
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Validates @param name matching and @returns presence on a function-like
|
|
133
|
+
* node that already has an attached JSDoc comment. Destructured, default-
|
|
134
|
+
* valued, and rest params are silently skipped to match historical behavior
|
|
135
|
+
* of the top-level export check.
|
|
136
|
+
*/
|
|
137
|
+
function validateParamsAndReturns(fnNode, jsDoc, issues, ctx) {
|
|
138
|
+
const docText = jsDoc.value;
|
|
139
|
+
if (!ctx.skipParams) {
|
|
140
|
+
const actualParams = fnNode.params
|
|
141
|
+
.map((p) => (p.type === 'Identifier' ? p.name : null))
|
|
142
|
+
.filter((n) => n !== null);
|
|
143
|
+
if (actualParams.length > 0) {
|
|
144
|
+
const docParams = extractParamNames(docText);
|
|
145
|
+
for (const param of actualParams) {
|
|
146
|
+
if (!docParams.includes(param)) {
|
|
147
|
+
issues.push({
|
|
148
|
+
line: ctx.line,
|
|
149
|
+
check: ctx.paramCheck,
|
|
150
|
+
message: `Exported ${ctx.label} at line ${ctx.line}: @param "${param}" is missing or misnamed in JSDoc.`,
|
|
151
|
+
...(ctx.severity ? { severity: ctx.severity } : {}),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (!ctx.skipReturns && functionReturnsValue(fnNode) && !hasReturnsTag(docText)) {
|
|
158
|
+
issues.push({
|
|
159
|
+
line: ctx.line,
|
|
160
|
+
check: ctx.returnsCheck,
|
|
161
|
+
message: `Exported ${ctx.label} at line ${ctx.line} returns a value but JSDoc is missing @returns.`,
|
|
162
|
+
...(ctx.severity ? { severity: ctx.severity } : {}),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
128
166
|
// ---------------------------------------------------------------------------
|
|
129
|
-
//
|
|
167
|
+
// Top-level export validation
|
|
130
168
|
// ---------------------------------------------------------------------------
|
|
131
|
-
|
|
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) {
|
|
132
185
|
const name = fn.id.name;
|
|
133
186
|
const start = lookupStart !== undefined ? lookupStart : fn.start;
|
|
134
187
|
const line = lineAt(source, start);
|
|
@@ -141,31 +194,20 @@ function validateFunctionDecl(fn, comments, source, issues, lookupStart) {
|
|
|
141
194
|
});
|
|
142
195
|
return;
|
|
143
196
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
for (const param of actualParams) {
|
|
151
|
-
if (!docParams.includes(param)) {
|
|
152
|
-
issues.push({
|
|
153
|
-
line,
|
|
154
|
-
check: 'jsdoc-param-mismatch',
|
|
155
|
-
message: `Exported function "${name}" at line ${line}: @param "${param}" is missing or misnamed in JSDoc.`,
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (functionReturnsValue(fn) && !hasReturnsTag(docText)) {
|
|
161
|
-
issues.push({
|
|
162
|
-
line,
|
|
163
|
-
check: 'jsdoc-missing-returns',
|
|
164
|
-
message: `Exported function "${name}" at line ${line} returns a value but JSDoc is missing @returns.`,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
197
|
+
validateParamsAndReturns(fn, jsDoc, issues, {
|
|
198
|
+
label: `function "${name}"`,
|
|
199
|
+
line,
|
|
200
|
+
paramCheck: 'jsdoc-param-mismatch',
|
|
201
|
+
returnsCheck: 'jsdoc-missing-returns',
|
|
202
|
+
});
|
|
167
203
|
}
|
|
168
|
-
|
|
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) {
|
|
169
211
|
const name = cls.id.name;
|
|
170
212
|
const start = lookupStart !== undefined ? lookupStart : cls.start;
|
|
171
213
|
const line = lineAt(source, start);
|
|
@@ -194,15 +236,102 @@ function validateVariableDecl(varDecl, comments, source, issues, lookupStart) {
|
|
|
194
236
|
}
|
|
195
237
|
}
|
|
196
238
|
// ---------------------------------------------------------------------------
|
|
239
|
+
// Class-method validation (opt-in via classMethodMode)
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
/**
|
|
242
|
+
* Returns the method's identifier name when statically resolvable, or
|
|
243
|
+
* undefined for computed keys (e.g. `[Symbol.iterator]()`). Private fields
|
|
244
|
+
* (`#name`) are treated as private-by-syntax and surface as undefined here
|
|
245
|
+
* so the walker skips them up front.
|
|
246
|
+
*/
|
|
247
|
+
function staticMethodName(method) {
|
|
248
|
+
if (method.computed)
|
|
249
|
+
return undefined;
|
|
250
|
+
const key = method.key;
|
|
251
|
+
if (key.type !== 'Identifier')
|
|
252
|
+
return undefined;
|
|
253
|
+
return key.name;
|
|
254
|
+
}
|
|
255
|
+
function isPrivateMethodKey(method) {
|
|
256
|
+
return method.key.type === 'PrivateIdentifier';
|
|
257
|
+
}
|
|
258
|
+
function classMethodLabel(className, method, name) {
|
|
259
|
+
if (method.kind === 'constructor')
|
|
260
|
+
return `constructor of class "${className}"`;
|
|
261
|
+
const prefix = method.static ? 'static ' : '';
|
|
262
|
+
if (method.kind === 'get')
|
|
263
|
+
return `${prefix}getter "${className}.${name}"`;
|
|
264
|
+
if (method.kind === 'set')
|
|
265
|
+
return `${prefix}setter "${className}.${name}"`;
|
|
266
|
+
return `${prefix}method "${className}.${name}"`;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Walks an exported class body and emits class-method JSDoc issues per
|
|
270
|
+
* the configured severity. Skip rules (in evaluation order):
|
|
271
|
+
* 1. private syntax (`#foo`) and underscore-prefixed names
|
|
272
|
+
* 2. zero-parameter constructors
|
|
273
|
+
* 3. methods whose JSDoc carries `@private` or `@internal`
|
|
274
|
+
*
|
|
275
|
+
* Pure-override skip (`super.method(...args)`-only bodies bypassing the
|
|
276
|
+
* @returns check) is deferred — V1 keeps the rule simple.
|
|
277
|
+
*/
|
|
278
|
+
export function validateClassMethods(cls, comments, source, issues, severity) {
|
|
279
|
+
const className = cls.id.name;
|
|
280
|
+
for (const member of cls.body.body) {
|
|
281
|
+
if (member.type !== 'MethodDefinition')
|
|
282
|
+
continue;
|
|
283
|
+
const method = member;
|
|
284
|
+
if (isPrivateMethodKey(method))
|
|
285
|
+
continue;
|
|
286
|
+
const name = staticMethodName(method);
|
|
287
|
+
if (name === undefined)
|
|
288
|
+
continue;
|
|
289
|
+
if (method.kind !== 'constructor' && name.startsWith('_'))
|
|
290
|
+
continue;
|
|
291
|
+
if (method.kind === 'constructor' && method.value.params.length === 0)
|
|
292
|
+
continue;
|
|
293
|
+
const methodStart = method.start;
|
|
294
|
+
const line = lineAt(source, methodStart);
|
|
295
|
+
const jsDoc = findAttachedJsDoc(comments, methodStart, source);
|
|
296
|
+
if (jsDoc && hasPrivateOrInternalTag(jsDoc.value))
|
|
297
|
+
continue;
|
|
298
|
+
const label = classMethodLabel(className, method, name);
|
|
299
|
+
if (!jsDoc) {
|
|
300
|
+
issues.push({
|
|
301
|
+
line,
|
|
302
|
+
check: 'missing-jsdoc-class-method',
|
|
303
|
+
message: `Exported ${label} at line ${line} is missing a JSDoc comment.`,
|
|
304
|
+
severity,
|
|
305
|
+
});
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (method.kind === 'get') {
|
|
309
|
+
// Presence already verified; getter expression is the contract.
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const skipReturns = method.kind === 'constructor' || method.kind === 'set';
|
|
313
|
+
validateParamsAndReturns(method.value, jsDoc, issues, {
|
|
314
|
+
label,
|
|
315
|
+
line,
|
|
316
|
+
paramCheck: 'jsdoc-class-method-param-mismatch',
|
|
317
|
+
returnsCheck: 'jsdoc-class-method-missing-returns',
|
|
318
|
+
severity,
|
|
319
|
+
skipReturns,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
197
324
|
// Public API
|
|
198
325
|
// ---------------------------------------------------------------------------
|
|
199
326
|
/**
|
|
200
327
|
* Validates JSDoc on exported declarations in a `.sys.mjs` source file.
|
|
201
328
|
*
|
|
202
329
|
* @param source - File content
|
|
330
|
+
* @param options - Optional gates for opt-in checks (e.g. class-method JSDoc)
|
|
203
331
|
* @returns Array of JSDoc issues found
|
|
204
332
|
*/
|
|
205
|
-
export function validateExportJsDoc(source) {
|
|
333
|
+
export function validateExportJsDoc(source, options) {
|
|
334
|
+
const classMethodMode = options?.classMethodMode ?? 'off';
|
|
206
335
|
const comments = [];
|
|
207
336
|
let ast;
|
|
208
337
|
try {
|
|
@@ -226,6 +355,9 @@ export function validateExportJsDoc(source) {
|
|
|
226
355
|
}
|
|
227
356
|
else if (decl.type === 'ClassDeclaration') {
|
|
228
357
|
validateClassDecl(decl, comments, source, issues, exportStart);
|
|
358
|
+
if (classMethodMode !== 'off') {
|
|
359
|
+
validateClassMethods(decl, comments, source, issues, classMethodMode);
|
|
360
|
+
}
|
|
229
361
|
}
|
|
230
362
|
else if (decl.type === 'VariableDeclaration') {
|
|
231
363
|
validateVariableDecl(decl, comments, source, issues, exportStart);
|
|
@@ -247,6 +379,9 @@ export function validateExportJsDoc(source) {
|
|
|
247
379
|
}
|
|
248
380
|
else if (localDecl.type === 'ClassDeclaration') {
|
|
249
381
|
validateClassDecl(localDecl, comments, source, issues);
|
|
382
|
+
if (classMethodMode !== 'off') {
|
|
383
|
+
validateClassMethods(localDecl, comments, source, issues, classMethodMode);
|
|
384
|
+
}
|
|
250
385
|
}
|
|
251
386
|
else {
|
|
252
387
|
validateVariableDecl(localDecl, comments, source, issues);
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Patch ownership resolution for
|
|
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
|
|
7
|
-
*
|
|
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>;
|
|
@@ -1,13 +1,41 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
|
-
* Patch ownership resolution for
|
|
3
|
+
* Patch ownership resolution for JS-shaped files.
|
|
4
4
|
*
|
|
5
5
|
* A file is "patch-owned" when it was created by the project's patch
|
|
6
6
|
* queue rather than being an upstream Firefox file that happens to be
|
|
7
|
-
* modified. This module computes the set of patch-owned
|
|
8
|
-
*
|
|
7
|
+
* modified. This module computes the set of patch-owned paths so lint
|
|
8
|
+
* rules can scope enforcement to project code only.
|
|
9
|
+
*
|
|
10
|
+
* The two resolvers are kept separate (one per extension predicate)
|
|
11
|
+
* because the downstream rules differ: `.sys.mjs` files go through
|
|
12
|
+
* `runCheckJs` (TypeScript checkJs) and the export-walker JSDoc rule;
|
|
13
|
+
* chrome subscripts (`.js` non-`.sys.mjs`) only get the script-walker
|
|
14
|
+
* JSDoc rule. Mixing them in a single set would silently broaden
|
|
15
|
+
* `runCheckJs` to chrome subscripts, which it is not designed for.
|
|
9
16
|
*/
|
|
10
17
|
import { collectNewFileCreatorsByPath } from './patch-lint-cross.js';
|
|
18
|
+
/**
|
|
19
|
+
* Returns the set of patch-owned files matching `predicate`. Internal
|
|
20
|
+
* helper shared by the per-extension resolvers below.
|
|
21
|
+
*/
|
|
22
|
+
function resolveOwned(currentNewFiles, patchQueueCtx, predicate) {
|
|
23
|
+
const owned = new Set();
|
|
24
|
+
for (const file of currentNewFiles) {
|
|
25
|
+
if (predicate(file)) {
|
|
26
|
+
owned.add(file);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (patchQueueCtx) {
|
|
30
|
+
const creators = collectNewFileCreatorsByPath(patchQueueCtx);
|
|
31
|
+
for (const [file, owners] of creators) {
|
|
32
|
+
if (predicate(file) && owners.length > 0) {
|
|
33
|
+
owned.add(file);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return owned;
|
|
38
|
+
}
|
|
11
39
|
/**
|
|
12
40
|
* Returns the set of file paths that are patch-owned `.sys.mjs` files.
|
|
13
41
|
*
|
|
@@ -24,20 +52,19 @@ import { collectNewFileCreatorsByPath } from './patch-lint-cross.js';
|
|
|
24
52
|
* @returns Set of patch-owned `.sys.mjs` file paths
|
|
25
53
|
*/
|
|
26
54
|
export function resolvePatchOwnedSysMjs(currentNewFiles, patchQueueCtx) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return owned;
|
|
55
|
+
return resolveOwned(currentNewFiles, patchQueueCtx, (file) => file.endsWith('.sys.mjs'));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Returns the set of file paths that are patch-owned chrome subscripts
|
|
59
|
+
* (`.js` files that are not `.sys.mjs` modules — typically
|
|
60
|
+
* `browser/base/content/<binaryName>*.js` and similar). Same ownership
|
|
61
|
+
* semantics as {@link resolvePatchOwnedSysMjs}.
|
|
62
|
+
*
|
|
63
|
+
* @param currentNewFiles - Files newly created in the current diff
|
|
64
|
+
* @param patchQueueCtx - Optional cross-patch context for queue-wide ownership
|
|
65
|
+
* @returns Set of patch-owned chrome-subscript file paths
|
|
66
|
+
*/
|
|
67
|
+
export function resolvePatchOwnedChromeScripts(currentNewFiles, patchQueueCtx) {
|
|
68
|
+
return resolveOwned(currentNewFiles, patchQueueCtx, (file) => file.endsWith('.js') && !file.endsWith('.sys.mjs'));
|
|
42
69
|
}
|
|
43
70
|
//# sourceMappingURL=patch-lint-ownership.js.map
|
|
@@ -5,7 +5,7 @@ export { runCheckJs } from './patch-lint-checkjs.js';
|
|
|
5
5
|
export { buildPatchQueueContext, collectNewFileCreatorsByPath, type ExtractedSpecifier, extractImportSpecifiers, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, FORWARD_IMPORT_IGNORE_MARKER, isForwardImportableFile, lintPatchQueue, lintPatchQueueDuplicateCreations, lintPatchQueueForwardImports, type PatchQueueContext, type PatchQueueEntry, } from './patch-lint-cross.js';
|
|
6
6
|
export { buildModifiedFileAdditionsFromDiff, detectNewFilesInDiff } from './patch-lint-diff.js';
|
|
7
7
|
export { type JsDocCheck, type JsDocIssue, validateExportJsDoc } from './patch-lint-jsdoc.js';
|
|
8
|
-
export { resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
|
|
8
|
+
export { resolvePatchOwnedChromeScripts, resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
|
|
9
9
|
/**
|
|
10
10
|
* Counts the total lines in a unified diff and the number of non-binary
|
|
11
11
|
* text lines, so binary hunks do not inflate patch size checks.
|
|
@@ -55,9 +55,14 @@ export declare function lintNewFileHeaders(repoDir: string, newFiles: string[],
|
|
|
55
55
|
* @param newFiles - Set of files that are newly created in this patch
|
|
56
56
|
* @param config - Project configuration
|
|
57
57
|
* @param patchOwnedFiles - Optional set of patch-owned `.sys.mjs` paths for scoped JSDoc enforcement
|
|
58
|
+
* @param patchOwnedChromeScripts - Optional set of patch-owned chrome
|
|
59
|
+
* subscripts (`.js` non-`.sys.mjs`) for scoped chrome-script JSDoc
|
|
60
|
+
* enforcement. Built by {@link resolvePatchOwnedChromeScripts}; passed
|
|
61
|
+
* separately from `patchOwnedFiles` so the `runCheckJs` consumer (which
|
|
62
|
+
* only accepts `.sys.mjs`) is not silently broadened.
|
|
58
63
|
* @returns Array of lint issues
|
|
59
64
|
*/
|
|
60
|
-
export declare function lintPatchedJs(repoDir: string, affectedFiles: string[], newFiles: Set<string>, config: FireForgeConfig, patchOwnedFiles?: Set<string>): Promise<PatchLintIssue[]>;
|
|
65
|
+
export declare function lintPatchedJs(repoDir: string, affectedFiles: string[], newFiles: Set<string>, config: FireForgeConfig, patchOwnedFiles?: Set<string>, patchOwnedChromeScripts?: Set<string>): Promise<PatchLintIssue[]>;
|
|
61
66
|
/**
|
|
62
67
|
* Checks that modifications to existing (non-new) JS/MJS files include at
|
|
63
68
|
* least one `// BINARYNAME:` comment in the added lines.
|
|
@@ -7,10 +7,11 @@ import { hasRawCssColors, stripJsComments } from '../utils/regex.js';
|
|
|
7
7
|
import { loadFurnaceConfig } from './furnace-config.js';
|
|
8
8
|
import { containsUpstreamLicenseText, getLicenseHeader, hasAnyLicenseHeader, hasAnyLicenseHeaderAnyStyle, } from './license-headers.js';
|
|
9
9
|
import { runCheckJs } from './patch-lint-checkjs.js';
|
|
10
|
+
import { lintChromeScriptJsDocForFile } from './patch-lint-chrome-jsdoc.js';
|
|
10
11
|
import { detectNewFilesInDiff, extractAddedLinesPerFile } from './patch-lint-diff.js';
|
|
11
12
|
import { AGGREGATE_PATCH_FILE } from './patch-lint-diff-tag.js';
|
|
12
13
|
import { validateExportJsDoc } from './patch-lint-jsdoc.js';
|
|
13
|
-
import { resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
|
|
14
|
+
import { resolvePatchOwnedChromeScripts, resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Cross-patch lint re-exports
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
@@ -24,7 +25,7 @@ export { runCheckJs } from './patch-lint-checkjs.js';
|
|
|
24
25
|
export { buildPatchQueueContext, collectNewFileCreatorsByPath, extractImportSpecifiers, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, FORWARD_IMPORT_IGNORE_MARKER, isForwardImportableFile, lintPatchQueue, lintPatchQueueDuplicateCreations, lintPatchQueueForwardImports, } from './patch-lint-cross.js';
|
|
25
26
|
export { buildModifiedFileAdditionsFromDiff, detectNewFilesInDiff } from './patch-lint-diff.js';
|
|
26
27
|
export { validateExportJsDoc } from './patch-lint-jsdoc.js';
|
|
27
|
-
export { resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
|
|
28
|
+
export { resolvePatchOwnedChromeScripts, resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
// Helpers
|
|
30
31
|
// ---------------------------------------------------------------------------
|
|
@@ -178,6 +179,21 @@ export function isTestFile(file) {
|
|
|
178
179
|
const basename = file.split('/').pop() ?? '';
|
|
179
180
|
return /^(?:browser_|test_|xpcshell_).*\.js$/.test(basename);
|
|
180
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Narrower scope than `isTestFile` — only browser-chrome test files
|
|
184
|
+
* (`browser_*.js` under a `/test/` or `/tests/` directory). Excludes
|
|
185
|
+
* `head.js` and `head_*.js` test helpers.
|
|
186
|
+
*/
|
|
187
|
+
function isBrowserChromeTestFile(file) {
|
|
188
|
+
if (!file.endsWith('.js'))
|
|
189
|
+
return false;
|
|
190
|
+
if (!/\/(?:test|tests)\//.test(file))
|
|
191
|
+
return false;
|
|
192
|
+
const basename = file.split('/').pop() ?? '';
|
|
193
|
+
if (basename === 'head.js' || /^head_.*\.js$/.test(basename))
|
|
194
|
+
return false;
|
|
195
|
+
return basename.startsWith('browser_');
|
|
196
|
+
}
|
|
181
197
|
/**
|
|
182
198
|
* Detects comment style from file extension for license header checks.
|
|
183
199
|
*/
|
|
@@ -388,9 +404,14 @@ export async function lintNewFileHeaders(repoDir, newFiles, config) {
|
|
|
388
404
|
* @param newFiles - Set of files that are newly created in this patch
|
|
389
405
|
* @param config - Project configuration
|
|
390
406
|
* @param patchOwnedFiles - Optional set of patch-owned `.sys.mjs` paths for scoped JSDoc enforcement
|
|
407
|
+
* @param patchOwnedChromeScripts - Optional set of patch-owned chrome
|
|
408
|
+
* subscripts (`.js` non-`.sys.mjs`) for scoped chrome-script JSDoc
|
|
409
|
+
* enforcement. Built by {@link resolvePatchOwnedChromeScripts}; passed
|
|
410
|
+
* separately from `patchOwnedFiles` so the `runCheckJs` consumer (which
|
|
411
|
+
* only accepts `.sys.mjs`) is not silently broadened.
|
|
391
412
|
* @returns Array of lint issues
|
|
392
413
|
*/
|
|
393
|
-
export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, patchOwnedFiles) {
|
|
414
|
+
export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, patchOwnedFiles, patchOwnedChromeScripts) {
|
|
394
415
|
const jsFiles = affectedFiles.filter(isJsFile);
|
|
395
416
|
if (jsFiles.length === 0)
|
|
396
417
|
return [];
|
|
@@ -450,13 +471,39 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
|
|
|
450
471
|
// 3. JSDoc on exports (patch-owned .sys.mjs files)
|
|
451
472
|
const isOwned = patchOwnedFiles ? patchOwnedFiles.has(file) : isNew;
|
|
452
473
|
if (isOwned && isSysMjs) {
|
|
453
|
-
const
|
|
474
|
+
const classMethodMode = config.patchLint?.jsdocClassMethods;
|
|
475
|
+
const jsdocIssues = validateExportJsDoc(content, classMethodMode ? { classMethodMode } : undefined);
|
|
454
476
|
for (const jsdocIssue of jsdocIssues) {
|
|
455
477
|
issues.push({
|
|
456
478
|
file,
|
|
457
479
|
check: jsdocIssue.check,
|
|
458
480
|
message: jsdocIssue.message,
|
|
459
|
-
severity: 'error',
|
|
481
|
+
severity: jsdocIssue.severity ?? 'error',
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// 3a. JSDoc on top-level declarations in patch-owned chrome subscripts.
|
|
486
|
+
// Dispatched out-of-line to keep this file under the per-file line
|
|
487
|
+
// budget. See `patch-lint-chrome-jsdoc.ts` for the rule body.
|
|
488
|
+
const isChromeOwned = file.endsWith('.js') &&
|
|
489
|
+
!isSysMjs &&
|
|
490
|
+
(patchOwnedChromeScripts ? patchOwnedChromeScripts.has(file) : isNew);
|
|
491
|
+
const chromeMode = config.patchLint?.chromeScriptJsDoc;
|
|
492
|
+
issues.push(...lintChromeScriptJsDocForFile(file, content, isChromeOwned, chromeMode));
|
|
493
|
+
// 3b. Assertion floor for browser-chrome tests touched by this patch
|
|
494
|
+
// (covers both newly introduced files and modified upstream tests —
|
|
495
|
+
// a patch that strips the last `Assert.equal` from an existing
|
|
496
|
+
// browser_*.js silently passed under the old `isNew` gate).
|
|
497
|
+
const assertionFloor = config.patchLint?.testAssertionFloor;
|
|
498
|
+
if (assertionFloor && assertionFloor !== 'off' && isBrowserChromeTestFile(file)) {
|
|
499
|
+
const ASSERTION_TOKENS = ['Assert.', 'ok(', 'is(', 'isnot(', 'isDeeply('];
|
|
500
|
+
const hasAssertion = ASSERTION_TOKENS.some((tok) => strippedContent.includes(tok));
|
|
501
|
+
if (!hasAssertion) {
|
|
502
|
+
issues.push({
|
|
503
|
+
file,
|
|
504
|
+
check: 'test-needs-assertion',
|
|
505
|
+
message: `Test file ${file} contains no assertions (Assert.*, ok(), is(), isnot(), isDeeply()). Smoke-only tests do not count as coverage. Add at least one assertion that pins user-visible behavior, or remove the file.`,
|
|
506
|
+
severity: assertionFloor,
|
|
460
507
|
});
|
|
461
508
|
}
|
|
462
509
|
}
|
|
@@ -673,10 +720,11 @@ export async function lintExportedPatch(repoDir, affectedFiles, diffContent, con
|
|
|
673
720
|
const newFiles = detectNewFilesInDiff(diffContent);
|
|
674
721
|
const { textLines: lineCount } = countNonBinaryDiffLines(diffContent);
|
|
675
722
|
const patchOwnedFiles = resolvePatchOwnedSysMjs(newFiles, patchQueueCtx);
|
|
723
|
+
const patchOwnedChromeScripts = resolvePatchOwnedChromeScripts(newFiles, patchQueueCtx);
|
|
676
724
|
const [cssIssues, headerIssues, jsIssues, modifiedHeaderIssues] = await Promise.all([
|
|
677
725
|
lintPatchedCss(repoDir, affectedFiles, diffContent, config),
|
|
678
726
|
lintNewFileHeaders(repoDir, [...newFiles], config),
|
|
679
|
-
lintPatchedJs(repoDir, affectedFiles, newFiles, config, patchOwnedFiles),
|
|
727
|
+
lintPatchedJs(repoDir, affectedFiles, newFiles, config, patchOwnedFiles, patchOwnedChromeScripts),
|
|
680
728
|
lintModifiedFileHeaders(repoDir, affectedFiles, newFiles),
|
|
681
729
|
]);
|
|
682
730
|
const modCommentIssues = lintModificationComments(diffContent, config);
|
|
@@ -316,6 +316,14 @@ export interface TestOptions {
|
|
|
316
316
|
* values appear after `--headless` if both are set.
|
|
317
317
|
*/
|
|
318
318
|
machArg?: string[];
|
|
319
|
+
/**
|
|
320
|
+
* Override the Marionette control port (default 2828) used by the
|
|
321
|
+
* stale-browser probe, the `--doctor` preflight, and the auto-forwarded
|
|
322
|
+
* `--setpref=marionette.port=<n>` arg passed to mach. Set this when a
|
|
323
|
+
* stale process holds the default port and `kill` is not an option, or
|
|
324
|
+
* when a CI runner reserves a different port for parallel test runs.
|
|
325
|
+
*/
|
|
326
|
+
marionettePort?: number;
|
|
319
327
|
}
|
|
320
328
|
/**
|
|
321
329
|
* Options for the furnace apply command.
|
|
@@ -59,6 +59,11 @@ export interface WireConfig {
|
|
|
59
59
|
/** Subscript directory relative to engine/. Default: "browser/base/content" */
|
|
60
60
|
subscriptDir?: string;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Severity gate for opt-in patch-lint rules. `'off'` disables the rule;
|
|
64
|
+
* `'warning'` and `'error'` emit issues at the matching severity.
|
|
65
|
+
*/
|
|
66
|
+
export type PatchLintSeverityGate = 'off' | 'warning' | 'error';
|
|
62
67
|
/**
|
|
63
68
|
* Configuration for patch lint rules.
|
|
64
69
|
*/
|
|
@@ -67,6 +72,21 @@ export interface PatchLintConfig {
|
|
|
67
72
|
checkJs?: boolean;
|
|
68
73
|
/** File paths exempt from the raw-color-value check (exact or basename match) */
|
|
69
74
|
rawColorAllowlist?: string[];
|
|
75
|
+
/** Enforce JSDoc on class-method exports in patch-owned .sys.mjs files. Default: 'off'. */
|
|
76
|
+
jsdocClassMethods?: PatchLintSeverityGate;
|
|
77
|
+
/** Require ≥1 assertion in any patch-touched browser_*.js test file (new or modified). Default: 'off'. */
|
|
78
|
+
testAssertionFloor?: PatchLintSeverityGate;
|
|
79
|
+
/**
|
|
80
|
+
* Enforce JSDoc on top-level classes (and their methods) and functions
|
|
81
|
+
* in patch-owned chrome subscripts (`.js` files loaded via
|
|
82
|
+
* `Services.scriptloader.loadSubScript`, e.g.
|
|
83
|
+
* `browser/base/content/<binaryName>*.js`). Distinct from
|
|
84
|
+
* `jsdocClassMethods` because chrome subscripts are parsed as scripts,
|
|
85
|
+
* not ES modules — using one flag for both would silently disable the
|
|
86
|
+
* rule when a chrome subscript was fed to the module parser. Default:
|
|
87
|
+
* 'off'.
|
|
88
|
+
*/
|
|
89
|
+
chromeScriptJsDoc?: PatchLintSeverityGate;
|
|
70
90
|
}
|
|
71
91
|
/**
|
|
72
92
|
* Build mode for mach.
|