@hominis/fireforge 0.18.5 → 0.18.8
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/config.js +8 -3
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate.js +37 -16
- package/dist/src/core/furnace-registration-ast.js +1 -3
- package/dist/src/core/patch-lint-jsdoc.d.ts +10 -2
- package/dist/src/core/patch-lint-jsdoc.js +139 -25
- package/dist/src/core/patch-lint.js +32 -2
- package/dist/src/types/config.d.ts +9 -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>
|
|
@@ -66,10 +66,15 @@ function formatValue(value) {
|
|
|
66
66
|
if (value === undefined) {
|
|
67
67
|
return '(not set)';
|
|
68
68
|
}
|
|
69
|
-
if (
|
|
70
|
-
return
|
|
69
|
+
if (typeof value === 'string')
|
|
70
|
+
return value;
|
|
71
|
+
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
|
|
72
|
+
return String(value);
|
|
73
|
+
}
|
|
74
|
+
if (typeof value === 'symbol') {
|
|
75
|
+
return value.toString();
|
|
71
76
|
}
|
|
72
|
-
return
|
|
77
|
+
return JSON.stringify(value, null, 2);
|
|
73
78
|
}
|
|
74
79
|
/**
|
|
75
80
|
* Runs the config command to get or set configuration values.
|
|
@@ -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", "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,40 @@ 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
|
+
return out;
|
|
233
|
+
}
|
|
213
234
|
//# sourceMappingURL=config-validate.js.map
|
|
@@ -70,9 +70,7 @@ function isInsideDOMContentLoaded(ancestors, content) {
|
|
|
70
70
|
call.callee.property.type === 'Identifier' &&
|
|
71
71
|
call.callee.property.name === 'addEventListener') {
|
|
72
72
|
const firstArg = call.arguments[0];
|
|
73
|
-
if (firstArg &&
|
|
74
|
-
firstArg.type === 'Literal' &&
|
|
75
|
-
firstArg.value === 'DOMContentLoaded') {
|
|
73
|
+
if (firstArg && firstArg.type === 'Literal' && firstArg.value === 'DOMContentLoaded') {
|
|
76
74
|
return true;
|
|
77
75
|
}
|
|
78
76
|
// Check if "DOMContentLoaded" appears in the call's source (handles edge cases)
|
|
@@ -6,16 +6,24 @@
|
|
|
6
6
|
* Separated from `patch-lint.ts` to keep both files within the
|
|
7
7
|
* project's per-file line budget.
|
|
8
8
|
*/
|
|
9
|
-
export type JsDocCheck = 'missing-jsdoc' | 'jsdoc-param-mismatch' | 'jsdoc-missing-returns';
|
|
9
|
+
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
10
|
export interface JsDocIssue {
|
|
11
11
|
line: number;
|
|
12
12
|
check: JsDocCheck;
|
|
13
13
|
message: string;
|
|
14
|
+
/** Optional severity hint. When undefined, callers default to 'error'. */
|
|
15
|
+
severity?: 'error' | 'warning';
|
|
16
|
+
}
|
|
17
|
+
export type ClassMethodMode = 'off' | 'warning' | 'error';
|
|
18
|
+
export interface ValidateExportJsDocOptions {
|
|
19
|
+
/** Gate for class-method JSDoc enforcement. Default 'off' (no walking). */
|
|
20
|
+
classMethodMode?: ClassMethodMode;
|
|
14
21
|
}
|
|
15
22
|
/**
|
|
16
23
|
* Validates JSDoc on exported declarations in a `.sys.mjs` source file.
|
|
17
24
|
*
|
|
18
25
|
* @param source - File content
|
|
26
|
+
* @param options - Optional gates for opt-in checks (e.g. class-method JSDoc)
|
|
19
27
|
* @returns Array of JSDoc issues found
|
|
20
28
|
*/
|
|
21
|
-
export declare function validateExportJsDoc(source: string): JsDocIssue[];
|
|
29
|
+
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,8 +128,43 @@ 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
|
function validateFunctionDecl(fn, comments, source, issues, lookupStart) {
|
|
132
170
|
const name = fn.id.name;
|
|
@@ -141,29 +179,12 @@ function validateFunctionDecl(fn, comments, source, issues, lookupStart) {
|
|
|
141
179
|
});
|
|
142
180
|
return;
|
|
143
181
|
}
|
|
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
|
-
}
|
|
182
|
+
validateParamsAndReturns(fn, jsDoc, issues, {
|
|
183
|
+
label: `function "${name}"`,
|
|
184
|
+
line,
|
|
185
|
+
paramCheck: 'jsdoc-param-mismatch',
|
|
186
|
+
returnsCheck: 'jsdoc-missing-returns',
|
|
187
|
+
});
|
|
167
188
|
}
|
|
168
189
|
function validateClassDecl(cls, comments, source, issues, lookupStart) {
|
|
169
190
|
const name = cls.id.name;
|
|
@@ -194,15 +215,102 @@ function validateVariableDecl(varDecl, comments, source, issues, lookupStart) {
|
|
|
194
215
|
}
|
|
195
216
|
}
|
|
196
217
|
// ---------------------------------------------------------------------------
|
|
218
|
+
// Class-method validation (opt-in via classMethodMode)
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
/**
|
|
221
|
+
* Returns the method's identifier name when statically resolvable, or
|
|
222
|
+
* undefined for computed keys (e.g. `[Symbol.iterator]()`). Private fields
|
|
223
|
+
* (`#name`) are treated as private-by-syntax and surface as undefined here
|
|
224
|
+
* so the walker skips them up front.
|
|
225
|
+
*/
|
|
226
|
+
function staticMethodName(method) {
|
|
227
|
+
if (method.computed)
|
|
228
|
+
return undefined;
|
|
229
|
+
const key = method.key;
|
|
230
|
+
if (key.type !== 'Identifier')
|
|
231
|
+
return undefined;
|
|
232
|
+
return key.name;
|
|
233
|
+
}
|
|
234
|
+
function isPrivateMethodKey(method) {
|
|
235
|
+
return method.key.type === 'PrivateIdentifier';
|
|
236
|
+
}
|
|
237
|
+
function classMethodLabel(className, method, name) {
|
|
238
|
+
if (method.kind === 'constructor')
|
|
239
|
+
return `constructor of class "${className}"`;
|
|
240
|
+
const prefix = method.static ? 'static ' : '';
|
|
241
|
+
if (method.kind === 'get')
|
|
242
|
+
return `${prefix}getter "${className}.${name}"`;
|
|
243
|
+
if (method.kind === 'set')
|
|
244
|
+
return `${prefix}setter "${className}.${name}"`;
|
|
245
|
+
return `${prefix}method "${className}.${name}"`;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Walks an exported class body and emits class-method JSDoc issues per
|
|
249
|
+
* the configured severity. Skip rules (in evaluation order):
|
|
250
|
+
* 1. private syntax (`#foo`) and underscore-prefixed names
|
|
251
|
+
* 2. zero-parameter constructors
|
|
252
|
+
* 3. methods whose JSDoc carries `@private` or `@internal`
|
|
253
|
+
*
|
|
254
|
+
* Pure-override skip (`super.method(...args)`-only bodies bypassing the
|
|
255
|
+
* @returns check) is deferred — V1 keeps the rule simple.
|
|
256
|
+
*/
|
|
257
|
+
function validateClassMethods(cls, comments, source, issues, severity) {
|
|
258
|
+
const className = cls.id.name;
|
|
259
|
+
for (const member of cls.body.body) {
|
|
260
|
+
if (member.type !== 'MethodDefinition')
|
|
261
|
+
continue;
|
|
262
|
+
const method = member;
|
|
263
|
+
if (isPrivateMethodKey(method))
|
|
264
|
+
continue;
|
|
265
|
+
const name = staticMethodName(method);
|
|
266
|
+
if (name === undefined)
|
|
267
|
+
continue;
|
|
268
|
+
if (method.kind !== 'constructor' && name.startsWith('_'))
|
|
269
|
+
continue;
|
|
270
|
+
if (method.kind === 'constructor' && method.value.params.length === 0)
|
|
271
|
+
continue;
|
|
272
|
+
const methodStart = method.start;
|
|
273
|
+
const line = lineAt(source, methodStart);
|
|
274
|
+
const jsDoc = findAttachedJsDoc(comments, methodStart, source);
|
|
275
|
+
if (jsDoc && hasPrivateOrInternalTag(jsDoc.value))
|
|
276
|
+
continue;
|
|
277
|
+
const label = classMethodLabel(className, method, name);
|
|
278
|
+
if (!jsDoc) {
|
|
279
|
+
issues.push({
|
|
280
|
+
line,
|
|
281
|
+
check: 'missing-jsdoc-class-method',
|
|
282
|
+
message: `Exported ${label} at line ${line} is missing a JSDoc comment.`,
|
|
283
|
+
severity,
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (method.kind === 'get') {
|
|
288
|
+
// Presence already verified; getter expression is the contract.
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const skipReturns = method.kind === 'constructor' || method.kind === 'set';
|
|
292
|
+
validateParamsAndReturns(method.value, jsDoc, issues, {
|
|
293
|
+
label,
|
|
294
|
+
line,
|
|
295
|
+
paramCheck: 'jsdoc-class-method-param-mismatch',
|
|
296
|
+
returnsCheck: 'jsdoc-class-method-missing-returns',
|
|
297
|
+
severity,
|
|
298
|
+
skipReturns,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
197
303
|
// Public API
|
|
198
304
|
// ---------------------------------------------------------------------------
|
|
199
305
|
/**
|
|
200
306
|
* Validates JSDoc on exported declarations in a `.sys.mjs` source file.
|
|
201
307
|
*
|
|
202
308
|
* @param source - File content
|
|
309
|
+
* @param options - Optional gates for opt-in checks (e.g. class-method JSDoc)
|
|
203
310
|
* @returns Array of JSDoc issues found
|
|
204
311
|
*/
|
|
205
|
-
export function validateExportJsDoc(source) {
|
|
312
|
+
export function validateExportJsDoc(source, options) {
|
|
313
|
+
const classMethodMode = options?.classMethodMode ?? 'off';
|
|
206
314
|
const comments = [];
|
|
207
315
|
let ast;
|
|
208
316
|
try {
|
|
@@ -226,6 +334,9 @@ export function validateExportJsDoc(source) {
|
|
|
226
334
|
}
|
|
227
335
|
else if (decl.type === 'ClassDeclaration') {
|
|
228
336
|
validateClassDecl(decl, comments, source, issues, exportStart);
|
|
337
|
+
if (classMethodMode !== 'off') {
|
|
338
|
+
validateClassMethods(decl, comments, source, issues, classMethodMode);
|
|
339
|
+
}
|
|
229
340
|
}
|
|
230
341
|
else if (decl.type === 'VariableDeclaration') {
|
|
231
342
|
validateVariableDecl(decl, comments, source, issues, exportStart);
|
|
@@ -247,6 +358,9 @@ export function validateExportJsDoc(source) {
|
|
|
247
358
|
}
|
|
248
359
|
else if (localDecl.type === 'ClassDeclaration') {
|
|
249
360
|
validateClassDecl(localDecl, comments, source, issues);
|
|
361
|
+
if (classMethodMode !== 'off') {
|
|
362
|
+
validateClassMethods(localDecl, comments, source, issues, classMethodMode);
|
|
363
|
+
}
|
|
250
364
|
}
|
|
251
365
|
else {
|
|
252
366
|
validateVariableDecl(localDecl, comments, source, issues);
|
|
@@ -178,6 +178,21 @@ export function isTestFile(file) {
|
|
|
178
178
|
const basename = file.split('/').pop() ?? '';
|
|
179
179
|
return /^(?:browser_|test_|xpcshell_).*\.js$/.test(basename);
|
|
180
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Narrower scope than `isTestFile` — only browser-chrome test files
|
|
183
|
+
* (`browser_*.js` under a `/test/` or `/tests/` directory). Excludes
|
|
184
|
+
* `head.js` and `head_*.js` test helpers.
|
|
185
|
+
*/
|
|
186
|
+
function isBrowserChromeTestFile(file) {
|
|
187
|
+
if (!file.endsWith('.js'))
|
|
188
|
+
return false;
|
|
189
|
+
if (!/\/(?:test|tests)\//.test(file))
|
|
190
|
+
return false;
|
|
191
|
+
const basename = file.split('/').pop() ?? '';
|
|
192
|
+
if (basename === 'head.js' || /^head_.*\.js$/.test(basename))
|
|
193
|
+
return false;
|
|
194
|
+
return basename.startsWith('browser_');
|
|
195
|
+
}
|
|
181
196
|
/**
|
|
182
197
|
* Detects comment style from file extension for license header checks.
|
|
183
198
|
*/
|
|
@@ -450,13 +465,28 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
|
|
|
450
465
|
// 3. JSDoc on exports (patch-owned .sys.mjs files)
|
|
451
466
|
const isOwned = patchOwnedFiles ? patchOwnedFiles.has(file) : isNew;
|
|
452
467
|
if (isOwned && isSysMjs) {
|
|
453
|
-
const
|
|
468
|
+
const classMethodMode = config.patchLint?.jsdocClassMethods;
|
|
469
|
+
const jsdocIssues = validateExportJsDoc(content, classMethodMode ? { classMethodMode } : undefined);
|
|
454
470
|
for (const jsdocIssue of jsdocIssues) {
|
|
455
471
|
issues.push({
|
|
456
472
|
file,
|
|
457
473
|
check: jsdocIssue.check,
|
|
458
474
|
message: jsdocIssue.message,
|
|
459
|
-
severity: 'error',
|
|
475
|
+
severity: jsdocIssue.severity ?? 'error',
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// 3b. Assertion floor for patch-introduced browser-chrome tests
|
|
480
|
+
const assertionFloor = config.patchLint?.testAssertionFloor;
|
|
481
|
+
if (assertionFloor && assertionFloor !== 'off' && isNew && isBrowserChromeTestFile(file)) {
|
|
482
|
+
const ASSERTION_TOKENS = ['Assert.', 'ok(', 'is(', 'isnot(', 'isDeeply('];
|
|
483
|
+
const hasAssertion = ASSERTION_TOKENS.some((tok) => strippedContent.includes(tok));
|
|
484
|
+
if (!hasAssertion) {
|
|
485
|
+
issues.push({
|
|
486
|
+
file,
|
|
487
|
+
check: 'test-needs-assertion',
|
|
488
|
+
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.`,
|
|
489
|
+
severity: assertionFloor,
|
|
460
490
|
});
|
|
461
491
|
}
|
|
462
492
|
}
|
|
@@ -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,10 @@ 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 patch-introduced browser_*.js test files. Default: 'off'. */
|
|
78
|
+
testAssertionFloor?: PatchLintSeverityGate;
|
|
70
79
|
}
|
|
71
80
|
/**
|
|
72
81
|
* Build mode for mach.
|