@hominis/fireforge 0.11.2 → 0.12.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 +24 -0
- package/README.md +54 -1
- package/dist/src/commands/export-shared.d.ts +2 -1
- package/dist/src/commands/export-shared.js +3 -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 +8 -3
- 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 +7 -2
- package/dist/src/core/patch-lint.js +30 -30
- package/dist/src/types/config.d.ts +9 -0
- package/dist/src/utils/paths.js +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Optional TypeScript `checkJs` pass for patch-owned `.sys.mjs` files.
|
|
4
|
+
*
|
|
5
|
+
* Loads the TypeScript compiler API via dynamic import so it is only
|
|
6
|
+
* required when `patchLint.checkJs` is enabled in `fireforge.json`.
|
|
7
|
+
* TypeScript remains a dev-dependency — if a user enables checkJs
|
|
8
|
+
* without installing it, the pass emits a clear error explaining
|
|
9
|
+
* how to fix it.
|
|
10
|
+
*
|
|
11
|
+
* Separated from `patch-lint.ts` to keep both files within the
|
|
12
|
+
* project's per-file line budget.
|
|
13
|
+
*/
|
|
14
|
+
import { resolve } from 'node:path';
|
|
15
|
+
import { pathExists } from '../utils/fs.js';
|
|
16
|
+
import { verbose } from '../utils/logger.js';
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Firefox globals shim
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const SHIM_FILENAME = '__fireforge_firefox_globals.d.ts';
|
|
21
|
+
/**
|
|
22
|
+
* Minimal `.d.ts` shim for Firefox privileged-scope globals.
|
|
23
|
+
*
|
|
24
|
+
* Firefox source is plain JS — no TypeScript allowed. The shim lets
|
|
25
|
+
* `checkJs` run without reporting "cannot find name" for the most
|
|
26
|
+
* common Mozilla APIs. Types are intentionally loose (`any`) because
|
|
27
|
+
* full Firefox type coverage is out of scope.
|
|
28
|
+
*
|
|
29
|
+
* Notable patterns that require shimming:
|
|
30
|
+
* - `const lazy = {};` + `ChromeUtils.defineESModuleGetters(lazy, { ... })`
|
|
31
|
+
* populates `lazy` at runtime; we declare it as `Record<string, any>`.
|
|
32
|
+
* - `Services.obs`, `Services.prefs`, etc. are XPCOM service accessors.
|
|
33
|
+
* - `Ci`, `Cc`, `Cr`, `Cu` are XPCOM component shortcuts.
|
|
34
|
+
* - Browser chrome globals like `gBrowser`, `gURLBar` are common in
|
|
35
|
+
* content scripts wired via `browser.js`.
|
|
36
|
+
*/
|
|
37
|
+
const FIREFOX_GLOBALS_SHIM = `
|
|
38
|
+
declare var Services: any;
|
|
39
|
+
declare var ChromeUtils: {
|
|
40
|
+
defineESModuleGetters(target: any, modules: Record<string, string>): void;
|
|
41
|
+
importESModule(specifier: string): any;
|
|
42
|
+
import(specifier: string): any;
|
|
43
|
+
defineModuleGetter(target: any, name: string, specifier: string): void;
|
|
44
|
+
generateQI(interfaces: any[]): Function;
|
|
45
|
+
isClassInfo(obj: any): boolean;
|
|
46
|
+
};
|
|
47
|
+
declare var Cu: any;
|
|
48
|
+
declare var Ci: any;
|
|
49
|
+
declare var Cc: any;
|
|
50
|
+
declare var Cr: any;
|
|
51
|
+
declare var Components: any;
|
|
52
|
+
declare var XPCOMUtils: any;
|
|
53
|
+
declare var lazy: Record<string, any>;
|
|
54
|
+
declare var PathUtils: any;
|
|
55
|
+
declare var IOUtils: any;
|
|
56
|
+
declare var FileUtils: any;
|
|
57
|
+
declare var gBrowser: any;
|
|
58
|
+
declare var gURLBar: any;
|
|
59
|
+
declare var gNavigatorBundle: any;
|
|
60
|
+
declare var AppConstants: any;
|
|
61
|
+
`;
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Diagnostic filtering
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
/**
|
|
66
|
+
* TS diagnostic codes to suppress because they are inherent to
|
|
67
|
+
* checking Firefox JS files outside of Mozilla's own build system.
|
|
68
|
+
*
|
|
69
|
+
* Firefox uses `resource://` and `chrome://` URL schemes for module
|
|
70
|
+
* imports. TypeScript's module resolver cannot follow these, so every
|
|
71
|
+
* import from an upstream Firefox module produces a spurious
|
|
72
|
+
* "Cannot find module" error. Filtering these out is essential to
|
|
73
|
+
* keep the checkJs pass usable — otherwise every file with an import
|
|
74
|
+
* would be buried in false positives.
|
|
75
|
+
*/
|
|
76
|
+
const SUPPRESSED_DIAGNOSTIC_CODES = new Set([
|
|
77
|
+
2307, // Cannot find module '{0}' or its corresponding type declarations.
|
|
78
|
+
2306, // File '{0}' is not a module.
|
|
79
|
+
2305, // Module '{0}' has no exported member '{1}'.
|
|
80
|
+
2792, // Cannot find module '{0}'. Did you mean to set the 'moduleResolution' option...
|
|
81
|
+
2304, // Cannot find name '{0}'. (for globals we missed in the shim)
|
|
82
|
+
2552, // Cannot find name '{0}'. Did you mean '{1}'?
|
|
83
|
+
2580, // Cannot find name '{0}'. Do you need to install type definitions...
|
|
84
|
+
7016, // Could not find a declaration file for module '{0}'.
|
|
85
|
+
]);
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Public API
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
/**
|
|
90
|
+
* Runs TypeScript's checkJs pass on patch-owned `.sys.mjs` files.
|
|
91
|
+
*
|
|
92
|
+
* @param repoDir - Absolute path to the engine (repository) directory
|
|
93
|
+
* @param patchOwnedFiles - Set of patch-owned `.sys.mjs` file paths (relative to repoDir)
|
|
94
|
+
* @returns Array of lint issues from TS diagnostics
|
|
95
|
+
*/
|
|
96
|
+
export async function runCheckJs(repoDir, patchOwnedFiles) {
|
|
97
|
+
if (patchOwnedFiles.size === 0)
|
|
98
|
+
return [];
|
|
99
|
+
// Dynamic import — typescript stays as a dev dependency
|
|
100
|
+
let ts;
|
|
101
|
+
try {
|
|
102
|
+
ts = await import('typescript');
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return [
|
|
106
|
+
{
|
|
107
|
+
file: '(checkJs)',
|
|
108
|
+
check: 'checkjs-type-error',
|
|
109
|
+
message: 'patchLint.checkJs is enabled but the "typescript" package is not installed. ' +
|
|
110
|
+
'Run "npm install typescript" to enable type checking.',
|
|
111
|
+
severity: 'error',
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
// Resolve absolute paths for root files, filtering to files that exist
|
|
116
|
+
const rootFiles = [];
|
|
117
|
+
const ownedAbsolute = new Set();
|
|
118
|
+
for (const rel of patchOwnedFiles) {
|
|
119
|
+
const abs = resolve(repoDir, rel);
|
|
120
|
+
if (await pathExists(abs)) {
|
|
121
|
+
rootFiles.push(abs);
|
|
122
|
+
ownedAbsolute.add(abs);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (rootFiles.length === 0)
|
|
126
|
+
return [];
|
|
127
|
+
const shimPath = resolve(repoDir, SHIM_FILENAME);
|
|
128
|
+
rootFiles.push(shimPath);
|
|
129
|
+
const options = {
|
|
130
|
+
allowJs: true,
|
|
131
|
+
checkJs: true,
|
|
132
|
+
noEmit: true,
|
|
133
|
+
strict: false,
|
|
134
|
+
target: ts.ScriptTarget.ESNext,
|
|
135
|
+
module: ts.ModuleKind.ESNext,
|
|
136
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
137
|
+
skipLibCheck: true,
|
|
138
|
+
// Do not follow import/reference directives into the Firefox tree.
|
|
139
|
+
// We only want to check the patch-owned files themselves.
|
|
140
|
+
// Without this, TS would try (and fail) to resolve every
|
|
141
|
+
// resource:// and chrome:// import, flooding the output with
|
|
142
|
+
// "Cannot find module" errors for upstream Firefox modules.
|
|
143
|
+
noResolve: true,
|
|
144
|
+
// Suppress implicit-any noise — Firefox code rarely has full type
|
|
145
|
+
// annotations and drowning users in thousands of implicit-any
|
|
146
|
+
// errors defeats the purpose of a focused check.
|
|
147
|
+
noImplicitAny: false,
|
|
148
|
+
};
|
|
149
|
+
// Custom compiler host: reads patch-owned files from disk, returns
|
|
150
|
+
// the shim for the shim path, and returns empty content for
|
|
151
|
+
// anything else to avoid reading the full Firefox tree.
|
|
152
|
+
const defaultHost = ts.createCompilerHost(options);
|
|
153
|
+
const host = {
|
|
154
|
+
...defaultHost,
|
|
155
|
+
getSourceFile(fileName, languageVersion, onError) {
|
|
156
|
+
if (fileName === shimPath) {
|
|
157
|
+
return ts.createSourceFile(fileName, FIREFOX_GLOBALS_SHIM, languageVersion, true);
|
|
158
|
+
}
|
|
159
|
+
if (ownedAbsolute.has(fileName)) {
|
|
160
|
+
return defaultHost.getSourceFile(fileName, languageVersion, onError);
|
|
161
|
+
}
|
|
162
|
+
// For lib files (lib.es*.d.ts) delegate to the default host
|
|
163
|
+
// so built-in types like Promise, Array, etc. are available.
|
|
164
|
+
if (fileName.includes('lib.') && fileName.endsWith('.d.ts')) {
|
|
165
|
+
return defaultHost.getSourceFile(fileName, languageVersion, onError);
|
|
166
|
+
}
|
|
167
|
+
// Return an empty source file for anything else to avoid
|
|
168
|
+
// reading unrelated Firefox source files.
|
|
169
|
+
return ts.createSourceFile(fileName, '', languageVersion, true);
|
|
170
|
+
},
|
|
171
|
+
fileExists(fileName) {
|
|
172
|
+
if (fileName === shimPath)
|
|
173
|
+
return true;
|
|
174
|
+
if (ownedAbsolute.has(fileName))
|
|
175
|
+
return true;
|
|
176
|
+
return defaultHost.fileExists(fileName);
|
|
177
|
+
},
|
|
178
|
+
readFile(fileName) {
|
|
179
|
+
if (fileName === shimPath)
|
|
180
|
+
return FIREFOX_GLOBALS_SHIM;
|
|
181
|
+
return defaultHost.readFile(fileName);
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
const program = ts.createProgram(rootFiles, options, host);
|
|
185
|
+
const allDiagnostics = [
|
|
186
|
+
...program.getSemanticDiagnostics(),
|
|
187
|
+
...program.getSyntacticDiagnostics(),
|
|
188
|
+
];
|
|
189
|
+
// Filter to diagnostics originating in patch-owned files only,
|
|
190
|
+
// and suppress module-resolution / unknown-name noise that is
|
|
191
|
+
// inherent to checking Firefox JS outside Mozilla's build system.
|
|
192
|
+
const issues = [];
|
|
193
|
+
for (const diag of allDiagnostics) {
|
|
194
|
+
if (SUPPRESSED_DIAGNOSTIC_CODES.has(diag.code))
|
|
195
|
+
continue;
|
|
196
|
+
const sourceFile = diag.file;
|
|
197
|
+
if (!sourceFile)
|
|
198
|
+
continue;
|
|
199
|
+
if (!ownedAbsolute.has(sourceFile.fileName))
|
|
200
|
+
continue;
|
|
201
|
+
const lineInfo = sourceFile.getLineAndCharacterOfPosition(diag.start ?? 0);
|
|
202
|
+
const line = lineInfo.line + 1;
|
|
203
|
+
const messageText = typeof diag.messageText === 'string'
|
|
204
|
+
? diag.messageText
|
|
205
|
+
: ts.flattenDiagnosticMessageText(diag.messageText, '\n');
|
|
206
|
+
// Find the relative path for the issue
|
|
207
|
+
let relPath = sourceFile.fileName;
|
|
208
|
+
for (const [rel, abs] of [...patchOwnedFiles].map((r) => [r, resolve(repoDir, r)])) {
|
|
209
|
+
if (abs === sourceFile.fileName) {
|
|
210
|
+
relPath = rel;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const severity = diag.category === ts.DiagnosticCategory.Error ? 'error' : 'warning';
|
|
215
|
+
issues.push({
|
|
216
|
+
file: relPath,
|
|
217
|
+
check: 'checkjs-type-error',
|
|
218
|
+
message: `Line ${line}: ${messageText}`,
|
|
219
|
+
severity,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
verbose(`checkJs: analyzed ${rootFiles.length - 1} file(s), found ${issues.length} issue(s)`);
|
|
223
|
+
return issues;
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=patch-lint-checkjs.js.map
|
|
@@ -113,6 +113,7 @@ export declare function isForwardImportableFile(path: string): boolean;
|
|
|
113
113
|
* - `import "specifier"` (side-effect imports — the `from`
|
|
114
114
|
* clause is optional in the regex)
|
|
115
115
|
* - `import("specifier")` (dynamic imports)
|
|
116
|
+
* - ChromeUtils.importESModule("specifier")
|
|
116
117
|
* - ChromeUtils.defineESModuleGetters(obj, { Name: "specifier", ... })
|
|
117
118
|
*
|
|
118
119
|
* Returns the raw specifier strings — callers should take the leaf basename
|
|
@@ -156,6 +156,7 @@ export function isForwardImportableFile(path) {
|
|
|
156
156
|
* - `import "specifier"` (side-effect imports — the `from`
|
|
157
157
|
* clause is optional in the regex)
|
|
158
158
|
* - `import("specifier")` (dynamic imports)
|
|
159
|
+
* - ChromeUtils.importESModule("specifier")
|
|
159
160
|
* - ChromeUtils.defineESModuleGetters(obj, { Name: "specifier", ... })
|
|
160
161
|
*
|
|
161
162
|
* Returns the raw specifier strings — callers should take the leaf basename
|
|
@@ -282,6 +283,12 @@ export function extractImportSpecifiersWithLines(source) {
|
|
|
282
283
|
if (match[1])
|
|
283
284
|
results.push({ specifier: match[1], line: offsetToLine(match.index) });
|
|
284
285
|
}
|
|
286
|
+
// ChromeUtils.importESModule("resource://...") — Firefox single-module import
|
|
287
|
+
const chromeUtilsPattern = /ChromeUtils\.importESModule\s*\(\s*["']([^"']+)["']/g;
|
|
288
|
+
while ((match = chromeUtilsPattern.exec(stripped)) !== null) {
|
|
289
|
+
if (match[1])
|
|
290
|
+
results.push({ specifier: match[1], line: offsetToLine(match.index) });
|
|
291
|
+
}
|
|
285
292
|
collectGetterSpecifiers(stripped, results, offsetToLine);
|
|
286
293
|
return results;
|
|
287
294
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-based JSDoc validation for exported declarations in `.sys.mjs`
|
|
3
|
+
* modules. Uses Acorn (already a runtime dependency) to parse the
|
|
4
|
+
* module and inspects JSDoc comments via the `onComment` callback.
|
|
5
|
+
*
|
|
6
|
+
* Separated from `patch-lint.ts` to keep both files within the
|
|
7
|
+
* project's per-file line budget.
|
|
8
|
+
*/
|
|
9
|
+
export type JsDocCheck = 'missing-jsdoc' | 'jsdoc-param-mismatch' | 'jsdoc-missing-returns';
|
|
10
|
+
export interface JsDocIssue {
|
|
11
|
+
line: number;
|
|
12
|
+
check: JsDocCheck;
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Validates JSDoc on exported declarations in a `.sys.mjs` source file.
|
|
17
|
+
*
|
|
18
|
+
* @param source - File content
|
|
19
|
+
* @returns Array of JSDoc issues found
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateExportJsDoc(source: string): JsDocIssue[];
|
|
@@ -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,11 @@
|
|
|
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';
|
|
6
9
|
/**
|
|
7
10
|
* Detects comment style from file extension for license header checks.
|
|
8
11
|
*/
|
|
@@ -34,9 +37,10 @@ export declare function lintNewFileHeaders(repoDir: string, newFiles: string[],
|
|
|
34
37
|
* @param affectedFiles - File paths (relative to repoDir)
|
|
35
38
|
* @param newFiles - Set of files that are newly created in this patch
|
|
36
39
|
* @param config - Project configuration
|
|
40
|
+
* @param patchOwnedFiles - Optional set of patch-owned `.sys.mjs` paths for scoped JSDoc enforcement
|
|
37
41
|
* @returns Array of lint issues
|
|
38
42
|
*/
|
|
39
|
-
export declare function lintPatchedJs(repoDir: string, affectedFiles: string[], newFiles: Set<string>, config: FireForgeConfig): Promise<PatchLintIssue[]>;
|
|
43
|
+
export declare function lintPatchedJs(repoDir: string, affectedFiles: string[], newFiles: Set<string>, config: FireForgeConfig, patchOwnedFiles?: Set<string>): Promise<PatchLintIssue[]>;
|
|
40
44
|
/**
|
|
41
45
|
* Checks that modifications to existing (non-new) JS/MJS files include at
|
|
42
46
|
* least one `// BINARYNAME:` comment in the added lines.
|
|
@@ -67,6 +71,7 @@ export declare function lintModifiedFileHeaders(repoDir: string, affectedFiles:
|
|
|
67
71
|
* @param affectedFiles - File paths (relative to repoDir) affected by the patch
|
|
68
72
|
* @param diffContent - Raw unified diff string
|
|
69
73
|
* @param config - Project configuration
|
|
74
|
+
* @param patchQueueCtx - Optional cross-patch context for ownership resolution
|
|
70
75
|
* @returns Array of all lint issues found
|
|
71
76
|
*/
|
|
72
|
-
export declare function lintExportedPatch(repoDir: string, affectedFiles: string[], diffContent: string, config: FireForgeConfig): Promise<PatchLintIssue[]>;
|
|
77
|
+
export declare function lintExportedPatch(repoDir: string, affectedFiles: string[], diffContent: string, config: FireForgeConfig, patchQueueCtx?: import('./patch-lint-cross.js').PatchQueueContext): Promise<PatchLintIssue[]>;
|