@hominis/fireforge 0.11.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +79 -26
- package/dist/src/commands/bootstrap-checks.d.ts +16 -0
- package/dist/src/commands/bootstrap-checks.js +66 -0
- package/dist/src/commands/bootstrap.js +27 -9
- package/dist/src/commands/doctor.d.ts +8 -0
- package/dist/src/commands/doctor.js +7 -1
- package/dist/src/commands/export-flow.js +3 -11
- package/dist/src/commands/export-shared.d.ts +2 -1
- package/dist/src/commands/export-shared.js +7 -2
- package/dist/src/commands/furnace/create.js +1 -1
- package/dist/src/commands/furnace/deploy.js +1 -1
- package/dist/src/commands/furnace/override.js +1 -1
- package/dist/src/commands/furnace/refresh.js +1 -1
- package/dist/src/commands/furnace/remove.js +1 -1
- package/dist/src/commands/furnace/rename.js +1 -1
- package/dist/src/commands/furnace/scan.js +1 -1
- package/dist/src/commands/lint.js +12 -3
- package/dist/src/commands/patch/delete.js +1 -15
- package/dist/src/commands/patch/reorder.js +1 -9
- package/dist/src/commands/re-export.js +1 -17
- package/dist/src/commands/verify.js +2 -2
- package/dist/src/core/ast-utils.d.ts +10 -0
- package/dist/src/core/ast-utils.js +18 -0
- package/dist/src/core/config-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +3 -0
- package/dist/src/core/config-validate.js +21 -3
- package/dist/src/core/file-lock.js +39 -2
- package/dist/src/core/furnace-apply.js +2 -1
- package/dist/src/core/furnace-config.js +6 -2
- package/dist/src/core/patch-apply.js +26 -4
- package/dist/src/core/patch-lint-checkjs.d.ts +21 -0
- package/dist/src/core/patch-lint-checkjs.js +225 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -0
- package/dist/src/core/patch-lint-cross.js +7 -0
- package/dist/src/core/patch-lint-jsdoc.d.ts +21 -0
- package/dist/src/core/patch-lint-jsdoc.js +259 -0
- package/dist/src/core/patch-lint-ownership.d.ts +25 -0
- package/dist/src/core/patch-lint-ownership.js +43 -0
- package/dist/src/core/patch-lint.d.ts +14 -3
- package/dist/src/core/patch-lint.js +116 -47
- package/dist/src/core/patch-manifest-resolve.d.ts +5 -0
- package/dist/src/core/patch-manifest-resolve.js +12 -0
- package/dist/src/core/patch-manifest.d.ts +1 -0
- package/dist/src/core/patch-manifest.js +1 -0
- package/dist/src/types/commands/patches.d.ts +2 -2
- package/dist/src/types/config.d.ts +11 -0
- package/dist/src/utils/paths.js +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* AST-based JSDoc validation for exported declarations in `.sys.mjs`
|
|
4
|
+
* modules. Uses Acorn (already a runtime dependency) to parse the
|
|
5
|
+
* module and inspects JSDoc comments via the `onComment` callback.
|
|
6
|
+
*
|
|
7
|
+
* Separated from `patch-lint.ts` to keep both files within the
|
|
8
|
+
* project's per-file line budget.
|
|
9
|
+
*/
|
|
10
|
+
import { parseModule } from './ast-utils.js';
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// JSDoc comment helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
function isJsDocComment(comment) {
|
|
15
|
+
return comment.type === 'Block' && comment.value.startsWith('*');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Finds the JSDoc comment immediately preceding `declStart` in the
|
|
19
|
+
* source. "Immediately" means only whitespace and newlines may appear
|
|
20
|
+
* between the comment's closing delimiter and the declaration.
|
|
21
|
+
*/
|
|
22
|
+
function findAttachedJsDoc(comments, declStart, source) {
|
|
23
|
+
for (let i = comments.length - 1; i >= 0; i--) {
|
|
24
|
+
const c = comments[i];
|
|
25
|
+
if (!c || !isJsDocComment(c))
|
|
26
|
+
continue;
|
|
27
|
+
const commentEnd = c.end;
|
|
28
|
+
if (commentEnd > declStart)
|
|
29
|
+
continue;
|
|
30
|
+
const between = source.slice(commentEnd, declStart);
|
|
31
|
+
if (/^\s*$/.test(between))
|
|
32
|
+
return c;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// JSDoc tag parsing
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
function extractParamNames(jsDoc) {
|
|
41
|
+
const names = [];
|
|
42
|
+
const paramPattern = /@param\s+(?:\{[^}]*\}\s+)?(\w+)/g;
|
|
43
|
+
let m;
|
|
44
|
+
while ((m = paramPattern.exec(jsDoc)) !== null) {
|
|
45
|
+
if (m[1])
|
|
46
|
+
names.push(m[1]);
|
|
47
|
+
}
|
|
48
|
+
return names;
|
|
49
|
+
}
|
|
50
|
+
function hasReturnsTag(jsDoc) {
|
|
51
|
+
return /@returns?\b/.test(jsDoc);
|
|
52
|
+
}
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Return-statement detection
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
/**
|
|
57
|
+
* Returns true if any direct `ReturnStatement` in the function body
|
|
58
|
+
* has a non-null argument. Does not recurse into nested functions or
|
|
59
|
+
* arrow expressions which have their own return semantics.
|
|
60
|
+
*/
|
|
61
|
+
function functionReturnsValue(node) {
|
|
62
|
+
return walkForReturn(node.body);
|
|
63
|
+
}
|
|
64
|
+
function walkForReturn(node) {
|
|
65
|
+
if (node.type === 'ReturnStatement') {
|
|
66
|
+
return node.argument != null;
|
|
67
|
+
}
|
|
68
|
+
if (node.type === 'FunctionDeclaration' ||
|
|
69
|
+
node.type === 'FunctionExpression' ||
|
|
70
|
+
node.type === 'ArrowFunctionExpression') {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
for (const key of Object.keys(node)) {
|
|
74
|
+
if (key === 'type')
|
|
75
|
+
continue;
|
|
76
|
+
const val = node[key];
|
|
77
|
+
if (val && typeof val === 'object') {
|
|
78
|
+
if (Array.isArray(val)) {
|
|
79
|
+
for (const child of val) {
|
|
80
|
+
if (child && typeof child === 'object' && 'type' in child) {
|
|
81
|
+
if (walkForReturn(child))
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else if ('type' in val) {
|
|
87
|
+
if (walkForReturn(val))
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
function findLocalDeclaration(body, name) {
|
|
95
|
+
for (const stmt of body) {
|
|
96
|
+
if (stmt.type === 'FunctionDeclaration') {
|
|
97
|
+
const fn = stmt;
|
|
98
|
+
if (fn.id.name === name)
|
|
99
|
+
return stmt;
|
|
100
|
+
}
|
|
101
|
+
else if (stmt.type === 'ClassDeclaration') {
|
|
102
|
+
const cls = stmt;
|
|
103
|
+
if (cls.id.name === name)
|
|
104
|
+
return stmt;
|
|
105
|
+
}
|
|
106
|
+
else if (stmt.type === 'VariableDeclaration') {
|
|
107
|
+
const varDecl = stmt;
|
|
108
|
+
for (const d of varDecl.declarations) {
|
|
109
|
+
if (d.id.type === 'Identifier' && d.id.name === name) {
|
|
110
|
+
return varDecl;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Line number helpers
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
function lineAt(source, offset) {
|
|
121
|
+
let line = 1;
|
|
122
|
+
for (let i = 0; i < offset && i < source.length; i++) {
|
|
123
|
+
if (source[i] === '\n')
|
|
124
|
+
line++;
|
|
125
|
+
}
|
|
126
|
+
return line;
|
|
127
|
+
}
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Core validation
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
function validateFunctionDecl(fn, comments, source, issues, lookupStart) {
|
|
132
|
+
const name = fn.id.name;
|
|
133
|
+
const start = lookupStart !== undefined ? lookupStart : fn.start;
|
|
134
|
+
const line = lineAt(source, start);
|
|
135
|
+
const jsDoc = findAttachedJsDoc(comments, start, source);
|
|
136
|
+
if (!jsDoc) {
|
|
137
|
+
issues.push({
|
|
138
|
+
line,
|
|
139
|
+
check: 'missing-jsdoc',
|
|
140
|
+
message: `Exported function "${name}" at line ${line} is missing a JSDoc comment.`,
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const docText = jsDoc.value;
|
|
145
|
+
const actualParams = fn.params
|
|
146
|
+
.map((p) => (p.type === 'Identifier' ? p.name : null))
|
|
147
|
+
.filter((n) => n !== null);
|
|
148
|
+
if (actualParams.length > 0) {
|
|
149
|
+
const docParams = extractParamNames(docText);
|
|
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
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function validateClassDecl(cls, comments, source, issues, lookupStart) {
|
|
169
|
+
const name = cls.id.name;
|
|
170
|
+
const start = lookupStart !== undefined ? lookupStart : cls.start;
|
|
171
|
+
const line = lineAt(source, start);
|
|
172
|
+
const jsDoc = findAttachedJsDoc(comments, start, source);
|
|
173
|
+
if (!jsDoc) {
|
|
174
|
+
issues.push({
|
|
175
|
+
line,
|
|
176
|
+
check: 'missing-jsdoc',
|
|
177
|
+
message: `Exported class "${name}" at line ${line} is missing a JSDoc comment.`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function validateVariableDecl(varDecl, comments, source, issues, lookupStart) {
|
|
182
|
+
const start = lookupStart !== undefined ? lookupStart : varDecl.start;
|
|
183
|
+
const jsDoc = findAttachedJsDoc(comments, start, source);
|
|
184
|
+
if (jsDoc)
|
|
185
|
+
return; // has a JSDoc block — sufficient for constants
|
|
186
|
+
for (const decl of varDecl.declarations) {
|
|
187
|
+
const name = decl.id.type === 'Identifier' ? decl.id.name : '<destructured>';
|
|
188
|
+
const line = lineAt(source, start);
|
|
189
|
+
issues.push({
|
|
190
|
+
line,
|
|
191
|
+
check: 'missing-jsdoc',
|
|
192
|
+
message: `Exported constant "${name}" at line ${line} is missing a JSDoc comment.`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Public API
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
/**
|
|
200
|
+
* Validates JSDoc on exported declarations in a `.sys.mjs` source file.
|
|
201
|
+
*
|
|
202
|
+
* @param source - File content
|
|
203
|
+
* @returns Array of JSDoc issues found
|
|
204
|
+
*/
|
|
205
|
+
export function validateExportJsDoc(source) {
|
|
206
|
+
const comments = [];
|
|
207
|
+
let ast;
|
|
208
|
+
try {
|
|
209
|
+
ast = parseModule(source, comments);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
const issues = [];
|
|
215
|
+
const body = ast.body;
|
|
216
|
+
for (const node of body) {
|
|
217
|
+
if (node.type !== 'ExportNamedDeclaration')
|
|
218
|
+
continue;
|
|
219
|
+
const exportNode = node;
|
|
220
|
+
// Case 1: inline export declaration — JSDoc attaches to `export`
|
|
221
|
+
if (exportNode.declaration) {
|
|
222
|
+
const decl = exportNode.declaration;
|
|
223
|
+
const exportStart = exportNode.start;
|
|
224
|
+
if (decl.type === 'FunctionDeclaration') {
|
|
225
|
+
validateFunctionDecl(decl, comments, source, issues, exportStart);
|
|
226
|
+
}
|
|
227
|
+
else if (decl.type === 'ClassDeclaration') {
|
|
228
|
+
validateClassDecl(decl, comments, source, issues, exportStart);
|
|
229
|
+
}
|
|
230
|
+
else if (decl.type === 'VariableDeclaration') {
|
|
231
|
+
validateVariableDecl(decl, comments, source, issues, exportStart);
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
// Case 2: `export { foo, Bar }` — resolve back to local declarations
|
|
236
|
+
if (exportNode.specifiers.length > 0 && !exportNode.source) {
|
|
237
|
+
for (const spec of exportNode.specifiers) {
|
|
238
|
+
const local = spec.local;
|
|
239
|
+
if (local.type !== 'Identifier')
|
|
240
|
+
continue;
|
|
241
|
+
const localName = local.name;
|
|
242
|
+
const localDecl = findLocalDeclaration(body, localName);
|
|
243
|
+
if (!localDecl)
|
|
244
|
+
continue;
|
|
245
|
+
if (localDecl.type === 'FunctionDeclaration') {
|
|
246
|
+
validateFunctionDecl(localDecl, comments, source, issues);
|
|
247
|
+
}
|
|
248
|
+
else if (localDecl.type === 'ClassDeclaration') {
|
|
249
|
+
validateClassDecl(localDecl, comments, source, issues);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
validateVariableDecl(localDecl, comments, source, issues);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return issues;
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=patch-lint-jsdoc.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch ownership resolution for `.sys.mjs` files.
|
|
3
|
+
*
|
|
4
|
+
* A file is "patch-owned" when it was created by the project's patch
|
|
5
|
+
* queue rather than being an upstream Firefox file that happens to be
|
|
6
|
+
* modified. This module computes the set of patch-owned `.sys.mjs`
|
|
7
|
+
* paths so lint rules can scope enforcement to project code only.
|
|
8
|
+
*/
|
|
9
|
+
import type { PatchQueueContext } from './patch-lint-cross.js';
|
|
10
|
+
/**
|
|
11
|
+
* Returns the set of file paths that are patch-owned `.sys.mjs` files.
|
|
12
|
+
*
|
|
13
|
+
* A file is patch-owned if:
|
|
14
|
+
* 1. It is newly created in the current diff, OR
|
|
15
|
+
* 2. It was created by an existing patch already in the queue.
|
|
16
|
+
*
|
|
17
|
+
* When no queue context is provided the result is limited to (1),
|
|
18
|
+
* which matches the pre-ownership behavior and keeps callers that
|
|
19
|
+
* do not have access to the patches directory working correctly.
|
|
20
|
+
*
|
|
21
|
+
* @param currentNewFiles - Files newly created in the current diff
|
|
22
|
+
* @param patchQueueCtx - Optional cross-patch context for queue-wide ownership
|
|
23
|
+
* @returns Set of patch-owned `.sys.mjs` file paths
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolvePatchOwnedSysMjs(currentNewFiles: Set<string>, patchQueueCtx?: PatchQueueContext): Set<string>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Patch ownership resolution for `.sys.mjs` files.
|
|
4
|
+
*
|
|
5
|
+
* A file is "patch-owned" when it was created by the project's patch
|
|
6
|
+
* queue rather than being an upstream Firefox file that happens to be
|
|
7
|
+
* modified. This module computes the set of patch-owned `.sys.mjs`
|
|
8
|
+
* paths so lint rules can scope enforcement to project code only.
|
|
9
|
+
*/
|
|
10
|
+
import { collectNewFileCreatorsByPath } from './patch-lint-cross.js';
|
|
11
|
+
/**
|
|
12
|
+
* Returns the set of file paths that are patch-owned `.sys.mjs` files.
|
|
13
|
+
*
|
|
14
|
+
* A file is patch-owned if:
|
|
15
|
+
* 1. It is newly created in the current diff, OR
|
|
16
|
+
* 2. It was created by an existing patch already in the queue.
|
|
17
|
+
*
|
|
18
|
+
* When no queue context is provided the result is limited to (1),
|
|
19
|
+
* which matches the pre-ownership behavior and keeps callers that
|
|
20
|
+
* do not have access to the patches directory working correctly.
|
|
21
|
+
*
|
|
22
|
+
* @param currentNewFiles - Files newly created in the current diff
|
|
23
|
+
* @param patchQueueCtx - Optional cross-patch context for queue-wide ownership
|
|
24
|
+
* @returns Set of patch-owned `.sys.mjs` file paths
|
|
25
|
+
*/
|
|
26
|
+
export function resolvePatchOwnedSysMjs(currentNewFiles, patchQueueCtx) {
|
|
27
|
+
const owned = new Set();
|
|
28
|
+
for (const file of currentNewFiles) {
|
|
29
|
+
if (file.endsWith('.sys.mjs')) {
|
|
30
|
+
owned.add(file);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (patchQueueCtx) {
|
|
34
|
+
const creators = collectNewFileCreatorsByPath(patchQueueCtx);
|
|
35
|
+
for (const [file, owners] of creators) {
|
|
36
|
+
if (file.endsWith('.sys.mjs') && owners.length > 0) {
|
|
37
|
+
owned.add(file);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return owned;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=patch-lint-ownership.js.map
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
2
2
|
import type { FireForgeConfig } from '../types/config.js';
|
|
3
3
|
import { type CommentStyle } from './license-headers.js';
|
|
4
|
+
export { runCheckJs } from './patch-lint-checkjs.js';
|
|
4
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';
|
|
5
6
|
export { buildModifiedFileAdditionsFromDiff, detectNewFilesInDiff } from './patch-lint-diff.js';
|
|
7
|
+
export { type JsDocCheck, type JsDocIssue, validateExportJsDoc } from './patch-lint-jsdoc.js';
|
|
8
|
+
export { resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
|
|
9
|
+
/**
|
|
10
|
+
* Returns true if the file path looks like a test file.
|
|
11
|
+
* Matches paths containing `/test/` or filenames starting with
|
|
12
|
+
* `browser_`, `test_`, or `xpcshell_` (all `.js`).
|
|
13
|
+
*/
|
|
14
|
+
export declare function isTestFile(file: string): boolean;
|
|
6
15
|
/**
|
|
7
16
|
* Detects comment style from file extension for license header checks.
|
|
8
17
|
*/
|
|
@@ -16,7 +25,7 @@ export declare function commentStyleForFile(file: string): CommentStyle | null;
|
|
|
16
25
|
* @param diffContent - Optional unified diff used to scope raw color checks to introduced lines
|
|
17
26
|
* @returns Array of lint issues found
|
|
18
27
|
*/
|
|
19
|
-
export declare function lintPatchedCss(repoDir: string, affectedFiles: string[], diffContent?: string): Promise<PatchLintIssue[]>;
|
|
28
|
+
export declare function lintPatchedCss(repoDir: string, affectedFiles: string[], diffContent?: string, config?: FireForgeConfig): Promise<PatchLintIssue[]>;
|
|
20
29
|
/**
|
|
21
30
|
* Checks new files for required license headers.
|
|
22
31
|
*
|
|
@@ -34,9 +43,10 @@ export declare function lintNewFileHeaders(repoDir: string, newFiles: string[],
|
|
|
34
43
|
* @param affectedFiles - File paths (relative to repoDir)
|
|
35
44
|
* @param newFiles - Set of files that are newly created in this patch
|
|
36
45
|
* @param config - Project configuration
|
|
46
|
+
* @param patchOwnedFiles - Optional set of patch-owned `.sys.mjs` paths for scoped JSDoc enforcement
|
|
37
47
|
* @returns Array of lint issues
|
|
38
48
|
*/
|
|
39
|
-
export declare function lintPatchedJs(repoDir: string, affectedFiles: string[], newFiles: Set<string>, config: FireForgeConfig): Promise<PatchLintIssue[]>;
|
|
49
|
+
export declare function lintPatchedJs(repoDir: string, affectedFiles: string[], newFiles: Set<string>, config: FireForgeConfig, patchOwnedFiles?: Set<string>): Promise<PatchLintIssue[]>;
|
|
40
50
|
/**
|
|
41
51
|
* Checks that modifications to existing (non-new) JS/MJS files include at
|
|
42
52
|
* least one `// BINARYNAME:` comment in the added lines.
|
|
@@ -67,6 +77,7 @@ export declare function lintModifiedFileHeaders(repoDir: string, affectedFiles:
|
|
|
67
77
|
* @param affectedFiles - File paths (relative to repoDir) affected by the patch
|
|
68
78
|
* @param diffContent - Raw unified diff string
|
|
69
79
|
* @param config - Project configuration
|
|
80
|
+
* @param patchQueueCtx - Optional cross-patch context for ownership resolution
|
|
70
81
|
* @returns Array of all lint issues found
|
|
71
82
|
*/
|
|
72
|
-
export declare function lintExportedPatch(repoDir: string, affectedFiles: string[], diffContent: string, config: FireForgeConfig): Promise<PatchLintIssue[]>;
|
|
83
|
+
export declare function lintExportedPatch(repoDir: string, affectedFiles: string[], diffContent: string, config: FireForgeConfig, patchQueueCtx?: import('./patch-lint-cross.js').PatchQueueContext): Promise<PatchLintIssue[]>;
|
|
@@ -6,7 +6,10 @@ import { verbose } from '../utils/logger.js';
|
|
|
6
6
|
import { hasRawCssColors, stripJsComments } from '../utils/regex.js';
|
|
7
7
|
import { loadFurnaceConfig } from './furnace-config.js';
|
|
8
8
|
import { getLicenseHeader, hasAnyLicenseHeader } from './license-headers.js';
|
|
9
|
+
import { runCheckJs } from './patch-lint-checkjs.js';
|
|
9
10
|
import { detectNewFilesInDiff, extractAddedLinesPerFile } from './patch-lint-diff.js';
|
|
11
|
+
import { validateExportJsDoc } from './patch-lint-jsdoc.js';
|
|
12
|
+
import { resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
|
|
10
13
|
// ---------------------------------------------------------------------------
|
|
11
14
|
// Cross-patch lint re-exports
|
|
12
15
|
// ---------------------------------------------------------------------------
|
|
@@ -16,12 +19,23 @@ import { detectNewFilesInDiff, extractAddedLinesPerFile } from './patch-lint-dif
|
|
|
16
19
|
// `patch-lint-cross.ts` so the per-patch and cross-patch rule bodies can
|
|
17
20
|
// each stay within the project's per-file line budget. Re-export the
|
|
18
21
|
// public surface so callers continue to import from a single module.
|
|
22
|
+
export { runCheckJs } from './patch-lint-checkjs.js';
|
|
19
23
|
export { buildPatchQueueContext, collectNewFileCreatorsByPath, extractImportSpecifiers, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, FORWARD_IMPORT_IGNORE_MARKER, isForwardImportableFile, lintPatchQueue, lintPatchQueueDuplicateCreations, lintPatchQueueForwardImports, } from './patch-lint-cross.js';
|
|
20
24
|
export { buildModifiedFileAdditionsFromDiff, detectNewFilesInDiff } from './patch-lint-diff.js';
|
|
25
|
+
export { validateExportJsDoc } from './patch-lint-jsdoc.js';
|
|
26
|
+
export { resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
|
|
21
27
|
// ---------------------------------------------------------------------------
|
|
22
28
|
// Helpers
|
|
23
29
|
// ---------------------------------------------------------------------------
|
|
24
30
|
const JS_EXTENSIONS = ['.js', '.mjs', '.jsm'];
|
|
31
|
+
const FILE_SIZE_THRESHOLDS = {
|
|
32
|
+
general: { notice: 500, warning: 750, error: 900 },
|
|
33
|
+
test: { notice: 1200, warning: 1400, error: 1600 },
|
|
34
|
+
};
|
|
35
|
+
const PATCH_LINE_THRESHOLDS = {
|
|
36
|
+
general: { notice: 800, warning: 1500, error: 3000 },
|
|
37
|
+
test: { notice: 1500, warning: 3000, error: 6000 },
|
|
38
|
+
};
|
|
25
39
|
/**
|
|
26
40
|
* Returns true if the filename looks like a JS/MJS/JSM file.
|
|
27
41
|
* Handles `.sys.mjs` as well.
|
|
@@ -29,6 +43,17 @@ const JS_EXTENSIONS = ['.js', '.mjs', '.jsm'];
|
|
|
29
43
|
function isJsFile(file) {
|
|
30
44
|
return JS_EXTENSIONS.some((ext) => file.endsWith(ext));
|
|
31
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Returns true if the file path looks like a test file.
|
|
48
|
+
* Matches paths containing `/test/` or filenames starting with
|
|
49
|
+
* `browser_`, `test_`, or `xpcshell_` (all `.js`).
|
|
50
|
+
*/
|
|
51
|
+
export function isTestFile(file) {
|
|
52
|
+
if (file.includes('/test/'))
|
|
53
|
+
return true;
|
|
54
|
+
const basename = file.split('/').pop() ?? '';
|
|
55
|
+
return /^(?:browser_|test_|xpcshell_).*\.js$/.test(basename);
|
|
56
|
+
}
|
|
32
57
|
/**
|
|
33
58
|
* Detects comment style from file extension for license header checks.
|
|
34
59
|
*/
|
|
@@ -53,7 +78,7 @@ export function commentStyleForFile(file) {
|
|
|
53
78
|
* @param diffContent - Optional unified diff used to scope raw color checks to introduced lines
|
|
54
79
|
* @returns Array of lint issues found
|
|
55
80
|
*/
|
|
56
|
-
export async function lintPatchedCss(repoDir, affectedFiles, diffContent) {
|
|
81
|
+
export async function lintPatchedCss(repoDir, affectedFiles, diffContent, config) {
|
|
57
82
|
const cssFiles = affectedFiles.filter((f) => f.endsWith('.css'));
|
|
58
83
|
if (cssFiles.length === 0)
|
|
59
84
|
return [];
|
|
@@ -80,17 +105,29 @@ export async function lintPatchedCss(repoDir, affectedFiles, diffContent) {
|
|
|
80
105
|
const rawCss = await readText(filePath);
|
|
81
106
|
// Strip block comments before scanning
|
|
82
107
|
const cssContent = rawCss.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
83
|
-
const rawColorContent = addedLinesByFile
|
|
84
|
-
? (addedLinesByFile.get(file) ?? []).join('\n').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
85
|
-
: cssContent;
|
|
86
108
|
// Check only introduced raw color values when diff context is available.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
109
|
+
// Skip files on the raw-color allowlist (exact path or basename match).
|
|
110
|
+
const allowlist = config?.patchLint?.rawColorAllowlist;
|
|
111
|
+
const isAllowlisted = allowlist?.some((entry) => file === entry || file.endsWith('/' + entry));
|
|
112
|
+
if (!isAllowlisted) {
|
|
113
|
+
// Strip lines with inline fireforge-ignore: raw-color-value suppression.
|
|
114
|
+
// Check against rawCss (before comment stripping) so the CSS comment marker is still present.
|
|
115
|
+
const sourceForSuppression = addedLinesByFile
|
|
116
|
+
? (addedLinesByFile.get(file) ?? []).join('\n')
|
|
117
|
+
: rawCss;
|
|
118
|
+
const suppressedContent = sourceForSuppression
|
|
119
|
+
.split('\n')
|
|
120
|
+
.filter((line) => !line.includes('fireforge-ignore: raw-color-value'))
|
|
121
|
+
.join('\n')
|
|
122
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
123
|
+
if (hasRawCssColors(suppressedContent)) {
|
|
124
|
+
issues.push({
|
|
125
|
+
file,
|
|
126
|
+
check: 'raw-color-value',
|
|
127
|
+
message: 'Raw color value found. Use CSS custom properties (var(--...)) for design token consistency.',
|
|
128
|
+
severity: 'error',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
94
131
|
}
|
|
95
132
|
// Check for non-tokenized custom properties
|
|
96
133
|
if (tokenPrefix) {
|
|
@@ -156,9 +193,10 @@ export async function lintNewFileHeaders(repoDir, newFiles, config) {
|
|
|
156
193
|
* @param affectedFiles - File paths (relative to repoDir)
|
|
157
194
|
* @param newFiles - Set of files that are newly created in this patch
|
|
158
195
|
* @param config - Project configuration
|
|
196
|
+
* @param patchOwnedFiles - Optional set of patch-owned `.sys.mjs` paths for scoped JSDoc enforcement
|
|
159
197
|
* @returns Array of lint issues
|
|
160
198
|
*/
|
|
161
|
-
export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config) {
|
|
199
|
+
export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, patchOwnedFiles) {
|
|
162
200
|
const jsFiles = affectedFiles.filter(isJsFile);
|
|
163
201
|
if (jsFiles.length === 0)
|
|
164
202
|
return [];
|
|
@@ -186,45 +224,50 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config) {
|
|
|
186
224
|
// 2. File size check (new files only)
|
|
187
225
|
if (isNew) {
|
|
188
226
|
const lineCount = content.split('\n').length;
|
|
189
|
-
|
|
227
|
+
const isTest = isTestFile(file);
|
|
228
|
+
const thresholds = isTest ? FILE_SIZE_THRESHOLDS.test : FILE_SIZE_THRESHOLDS.general;
|
|
229
|
+
const label = isTest ? 'Test file' : 'New file';
|
|
230
|
+
const verb = isTest ? 'splitting' : 'decomposing';
|
|
231
|
+
if (lineCount >= thresholds.error) {
|
|
190
232
|
issues.push({
|
|
191
233
|
file,
|
|
192
234
|
check: 'file-too-large',
|
|
193
|
-
message:
|
|
235
|
+
message: `${label} has ${lineCount} lines (hard limit: ${thresholds.error}). Consider ${verb}.`,
|
|
236
|
+
severity: 'error',
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
else if (lineCount >= thresholds.warning) {
|
|
240
|
+
issues.push({
|
|
241
|
+
file,
|
|
242
|
+
check: 'file-too-large',
|
|
243
|
+
message: `${label} has ${lineCount} lines (soft limit: ${thresholds.warning}, hard limit: ${thresholds.error}). Consider ${verb}.`,
|
|
194
244
|
severity: 'warning',
|
|
195
245
|
});
|
|
196
246
|
}
|
|
247
|
+
else if (lineCount >= thresholds.notice) {
|
|
248
|
+
issues.push({
|
|
249
|
+
file,
|
|
250
|
+
check: 'file-too-large',
|
|
251
|
+
message: `${label} has ${lineCount} lines (soft limit: ${thresholds.warning}, hard limit: ${thresholds.error}). Consider ${verb}.`,
|
|
252
|
+
severity: 'notice',
|
|
253
|
+
});
|
|
254
|
+
}
|
|
197
255
|
}
|
|
198
|
-
// 3. JSDoc on exports (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
continue;
|
|
210
|
-
if (prev.endsWith('*/')) {
|
|
211
|
-
hasJsDoc = true;
|
|
212
|
-
}
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
if (!hasJsDoc) {
|
|
216
|
-
issues.push({
|
|
217
|
-
file,
|
|
218
|
-
check: 'missing-jsdoc',
|
|
219
|
-
message: `Export at line ${i + 1} is missing a JSDoc comment with @param/@returns.`,
|
|
220
|
-
severity: 'warning',
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
}
|
|
256
|
+
// 3. JSDoc on exports (patch-owned .sys.mjs files)
|
|
257
|
+
const isOwned = patchOwnedFiles ? patchOwnedFiles.has(file) : isNew;
|
|
258
|
+
if (isOwned && isSysMjs) {
|
|
259
|
+
const jsdocIssues = validateExportJsDoc(content);
|
|
260
|
+
for (const jsdocIssue of jsdocIssues) {
|
|
261
|
+
issues.push({
|
|
262
|
+
file,
|
|
263
|
+
check: jsdocIssue.check,
|
|
264
|
+
message: jsdocIssue.message,
|
|
265
|
+
severity: 'error',
|
|
266
|
+
});
|
|
224
267
|
}
|
|
225
268
|
}
|
|
226
269
|
// 4. Observer topic naming
|
|
227
|
-
const topicPattern = /(?:addObserver|removeObserver|notifyObservers)\s*\([^)]*["']([^"']+)["']/g;
|
|
270
|
+
const topicPattern = /(?:addObserver|removeObserver|notifyObservers)\s*\([^)\n]*["']([^"']+)["']/g;
|
|
228
271
|
let topicMatch;
|
|
229
272
|
while ((topicMatch = topicPattern.exec(strippedContent)) !== null) {
|
|
230
273
|
const topic = topicMatch[1];
|
|
@@ -291,14 +334,32 @@ export function lintPatchSize(filesAffected, lineCount) {
|
|
|
291
334
|
severity: 'warning',
|
|
292
335
|
});
|
|
293
336
|
}
|
|
294
|
-
|
|
337
|
+
const allTests = filesAffected.length > 0 && filesAffected.every(isTestFile);
|
|
338
|
+
const thresholds = allTests ? PATCH_LINE_THRESHOLDS.test : PATCH_LINE_THRESHOLDS.general;
|
|
339
|
+
if (lineCount >= thresholds.error) {
|
|
340
|
+
issues.push({
|
|
341
|
+
file: '(patch)',
|
|
342
|
+
check: 'large-patch-lines',
|
|
343
|
+
message: `Patch is ${lineCount} lines (hard limit: ${thresholds.error}). Consider splitting into smaller, focused patches.`,
|
|
344
|
+
severity: 'error',
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
else if (lineCount >= thresholds.warning) {
|
|
295
348
|
issues.push({
|
|
296
349
|
file: '(patch)',
|
|
297
350
|
check: 'large-patch-lines',
|
|
298
|
-
message: `Patch is ${lineCount} lines (
|
|
351
|
+
message: `Patch is ${lineCount} lines (soft limit: ${thresholds.warning}, hard limit: ${thresholds.error}). Consider splitting into smaller, focused patches.`,
|
|
299
352
|
severity: 'warning',
|
|
300
353
|
});
|
|
301
354
|
}
|
|
355
|
+
else if (lineCount >= thresholds.notice) {
|
|
356
|
+
issues.push({
|
|
357
|
+
file: '(patch)',
|
|
358
|
+
check: 'large-patch-lines',
|
|
359
|
+
message: `Patch is ${lineCount} lines (soft limit: ${thresholds.warning}, hard limit: ${thresholds.error}). Consider splitting into smaller, focused patches.`,
|
|
360
|
+
severity: 'notice',
|
|
361
|
+
});
|
|
362
|
+
}
|
|
302
363
|
return issues;
|
|
303
364
|
}
|
|
304
365
|
// ---------------------------------------------------------------------------
|
|
@@ -346,20 +407,22 @@ export async function lintModifiedFileHeaders(repoDir, affectedFiles, newFiles)
|
|
|
346
407
|
* @param affectedFiles - File paths (relative to repoDir) affected by the patch
|
|
347
408
|
* @param diffContent - Raw unified diff string
|
|
348
409
|
* @param config - Project configuration
|
|
410
|
+
* @param patchQueueCtx - Optional cross-patch context for ownership resolution
|
|
349
411
|
* @returns Array of all lint issues found
|
|
350
412
|
*/
|
|
351
|
-
export async function lintExportedPatch(repoDir, affectedFiles, diffContent, config) {
|
|
413
|
+
export async function lintExportedPatch(repoDir, affectedFiles, diffContent, config, patchQueueCtx) {
|
|
352
414
|
const newFiles = detectNewFilesInDiff(diffContent);
|
|
353
415
|
const lineCount = diffContent.split('\n').length;
|
|
416
|
+
const patchOwnedFiles = resolvePatchOwnedSysMjs(newFiles, patchQueueCtx);
|
|
354
417
|
const [cssIssues, headerIssues, jsIssues, modifiedHeaderIssues] = await Promise.all([
|
|
355
|
-
lintPatchedCss(repoDir, affectedFiles, diffContent),
|
|
418
|
+
lintPatchedCss(repoDir, affectedFiles, diffContent, config),
|
|
356
419
|
lintNewFileHeaders(repoDir, [...newFiles], config),
|
|
357
|
-
lintPatchedJs(repoDir, affectedFiles, newFiles, config),
|
|
420
|
+
lintPatchedJs(repoDir, affectedFiles, newFiles, config, patchOwnedFiles),
|
|
358
421
|
lintModifiedFileHeaders(repoDir, affectedFiles, newFiles),
|
|
359
422
|
]);
|
|
360
423
|
const modCommentIssues = lintModificationComments(diffContent, config);
|
|
361
424
|
const sizeIssues = lintPatchSize(affectedFiles, lineCount);
|
|
362
|
-
|
|
425
|
+
const issues = [
|
|
363
426
|
...sizeIssues,
|
|
364
427
|
...cssIssues,
|
|
365
428
|
...headerIssues,
|
|
@@ -367,5 +430,11 @@ export async function lintExportedPatch(repoDir, affectedFiles, diffContent, con
|
|
|
367
430
|
...jsIssues,
|
|
368
431
|
...modCommentIssues,
|
|
369
432
|
];
|
|
433
|
+
// Optional checkJs pass — only when explicitly enabled in config
|
|
434
|
+
if (config.patchLint?.checkJs) {
|
|
435
|
+
const checkJsIssues = await runCheckJs(repoDir, patchOwnedFiles);
|
|
436
|
+
issues.push(...checkJsIssues);
|
|
437
|
+
}
|
|
438
|
+
return issues;
|
|
370
439
|
}
|
|
371
440
|
//# sourceMappingURL=patch-lint.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { PatchMetadata } from '../types/commands/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves a patch identifier (ordinal number or filename) to its manifest entry.
|
|
4
|
+
*/
|
|
5
|
+
export declare function resolvePatchIdentifier(identifier: string, patches: PatchMetadata[]): PatchMetadata | null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a patch identifier (ordinal number or filename) to its manifest entry.
|
|
3
|
+
*/
|
|
4
|
+
export function resolvePatchIdentifier(identifier, patches) {
|
|
5
|
+
if (/^\d+$/.test(identifier)) {
|
|
6
|
+
const order = parseInt(identifier, 10);
|
|
7
|
+
return patches.find((p) => p.order === order) ?? null;
|
|
8
|
+
}
|
|
9
|
+
const normalized = identifier.endsWith('.patch') ? identifier : `${identifier}.patch`;
|
|
10
|
+
return patches.find((p) => p.filename === normalized) ?? null;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=patch-manifest-resolve.js.map
|
|
@@ -8,4 +8,5 @@ export type { PatchManifestConsistencyIssue } from './patch-manifest-consistency
|
|
|
8
8
|
export { rebuildPatchesManifest, validatePatchesManifestConsistency, } from './patch-manifest-consistency.js';
|
|
9
9
|
export { addPatchToManifest, loadPatchesManifest, PatchDeleteRollbackError, PATCHES_MANIFEST, type PatchRenameEntry, removePatchFileAndManifest, removePatchFromManifest, renumberPatchesInManifest, savePatchesManifest, } from './patch-manifest-io.js';
|
|
10
10
|
export { checkVersionCompatibility, findPatchesAffectingFile, getClaimedFiles, stampPatchVersions, validatePatchIntegrity, } from './patch-manifest-query.js';
|
|
11
|
+
export { resolvePatchIdentifier } from './patch-manifest-resolve.js';
|
|
11
12
|
export { validatePatchesManifest } from './patch-manifest-validate.js';
|