@kingsword/lint-config 0.1.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/LICENSE +21 -0
- package/README.md +54 -0
- package/configs/oxlint/backend.json +3 -0
- package/configs/oxlint/base.json +42 -0
- package/configs/oxlint/frontend.json +34 -0
- package/configs/oxlint/fullstack.json +8 -0
- package/configs/oxlint/recommended.json +45 -0
- package/configs/oxlint/restricted-syntax-base.json +35 -0
- package/configs/oxlint/restricted-syntax-frontend.json +123 -0
- package/configs/oxlint/vitest-kingsword.json +10 -0
- package/configs/oxlint/vitest-strict.json +11 -0
- package/configs/oxlint/vitest.json +24 -0
- package/dist/chunk-CbDLau6x.cjs +34 -0
- package/dist/cli.cjs +106 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +106 -0
- package/dist/config.cjs +220 -0
- package/dist/config.d.cts +20 -0
- package/dist/config.d.mts +20 -0
- package/dist/config.mjs +214 -0
- package/dist/index.cjs +2003 -0
- package/dist/index.d.cts +53 -0
- package/dist/index.d.mts +54 -0
- package/dist/index.mjs +2000 -0
- package/docs/rules.zh-CN.md +491 -0
- package/package.json +92 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2003 @@
|
|
|
1
|
+
const require_chunk = require('./chunk-CbDLau6x.cjs');
|
|
2
|
+
let node_fs = require("node:fs");
|
|
3
|
+
node_fs = require_chunk.__toESM(node_fs);
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
node_path = require_chunk.__toESM(node_path);
|
|
6
|
+
|
|
7
|
+
//#region src/utils/options.ts
|
|
8
|
+
function getFirstOptionObject(context) {
|
|
9
|
+
const raw = Array.isArray(context.options) ? context.options[0] : void 0;
|
|
10
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) return raw;
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/rules/no-restricted-syntax.ts
|
|
16
|
+
/**
|
|
17
|
+
* A small subset of ESLint's `no-restricted-syntax`, implemented as a JS plugin rule.
|
|
18
|
+
*
|
|
19
|
+
* Config shape (ESLint compatible):
|
|
20
|
+
* "@kingsword/no-restricted-syntax": ["error", "ForInStatement", { selector, message }, ...]
|
|
21
|
+
*/
|
|
22
|
+
const rule$25 = {
|
|
23
|
+
meta: {
|
|
24
|
+
type: "problem",
|
|
25
|
+
docs: { description: "Disallow specific syntax patterns by selector." },
|
|
26
|
+
schema: {
|
|
27
|
+
type: "array",
|
|
28
|
+
items: { anyOf: [{ type: "string" }, {
|
|
29
|
+
type: "object",
|
|
30
|
+
properties: {
|
|
31
|
+
selector: { type: "string" },
|
|
32
|
+
message: { type: "string" }
|
|
33
|
+
},
|
|
34
|
+
required: ["selector"],
|
|
35
|
+
additionalProperties: false
|
|
36
|
+
}] }
|
|
37
|
+
},
|
|
38
|
+
messages: { restricted: "The syntax '{{selector}}' is not allowed." }
|
|
39
|
+
},
|
|
40
|
+
create(context) {
|
|
41
|
+
const restrictions = Array.isArray(context.options) ? context.options : [];
|
|
42
|
+
const listeners = {};
|
|
43
|
+
for (const restriction of restrictions) {
|
|
44
|
+
const restrictionObj = typeof restriction === "string" ? { selector: restriction } : getFirstOptionObject({
|
|
45
|
+
...context,
|
|
46
|
+
options: [restriction]
|
|
47
|
+
});
|
|
48
|
+
const selector = restrictionObj.selector;
|
|
49
|
+
if (!selector) continue;
|
|
50
|
+
const message = typeof restriction === "string" ? `The syntax '${selector}' is not allowed.` : restrictionObj.message || `The syntax '${selector}' is not allowed.`;
|
|
51
|
+
listeners[selector] = (node) => {
|
|
52
|
+
context.report({
|
|
53
|
+
node,
|
|
54
|
+
message
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return listeners;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var no_restricted_syntax_default = rule$25;
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/rules/no-exported-function-expressions.ts
|
|
65
|
+
const rule$24 = {
|
|
66
|
+
meta: {
|
|
67
|
+
type: "suggestion",
|
|
68
|
+
docs: { description: "Require exported functions to use function declarations instead of function expressions." },
|
|
69
|
+
schema: [],
|
|
70
|
+
messages: { useDeclaration: "Exported function expressions are not allowed. Use a function declaration instead." }
|
|
71
|
+
},
|
|
72
|
+
create(context) {
|
|
73
|
+
return {
|
|
74
|
+
ExportNamedDeclaration(node) {
|
|
75
|
+
const decl = node.declaration;
|
|
76
|
+
if (!decl || decl.type !== "VariableDeclaration") return;
|
|
77
|
+
for (const declarator of decl.declarations || []) {
|
|
78
|
+
const init = declarator && declarator.init;
|
|
79
|
+
if (!init) continue;
|
|
80
|
+
if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression") context.report({
|
|
81
|
+
node: declarator.id || decl,
|
|
82
|
+
messageId: "useDeclaration"
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
ExportDefaultDeclaration(node) {
|
|
87
|
+
const decl = node.declaration;
|
|
88
|
+
if (decl && (decl.type === "ArrowFunctionExpression" || decl.type === "FunctionExpression")) context.report({
|
|
89
|
+
node: decl,
|
|
90
|
+
messageId: "useDeclaration"
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var no_exported_function_expressions_default = rule$24;
|
|
97
|
+
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/rules/no-exported-string-union-types.ts
|
|
100
|
+
function isStringLiteralTsType(node) {
|
|
101
|
+
return node && node.type === "TSLiteralType" && node.literal && node.literal.type === "Literal" && typeof node.literal.value === "string";
|
|
102
|
+
}
|
|
103
|
+
function isExportedTypeDeclaration(node) {
|
|
104
|
+
const parent = node?.parent;
|
|
105
|
+
if (!parent) return false;
|
|
106
|
+
if (parent.type === "ExportNamedDeclaration" || parent.type === "ExportDefaultDeclaration") return true;
|
|
107
|
+
if (parent.type === "TSModuleBlock" && parent.parent && parent.parent.parent && parent.parent.parent.type === "ExportNamedDeclaration") return true;
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
const rule$23 = {
|
|
111
|
+
meta: {
|
|
112
|
+
type: "suggestion",
|
|
113
|
+
docs: { description: "Disallow exported types that are unions of string literals; prefer enums instead." },
|
|
114
|
+
schema: [],
|
|
115
|
+
messages: { preferEnum: "Exported string union types are not allowed. Prefer an enum (typically in enums.ts)." }
|
|
116
|
+
},
|
|
117
|
+
create(context) {
|
|
118
|
+
return { TSTypeAliasDeclaration(node) {
|
|
119
|
+
if (!isExportedTypeDeclaration(node)) return;
|
|
120
|
+
const typeAnn = node.typeAnnotation;
|
|
121
|
+
if (!typeAnn || typeAnn.type !== "TSUnionType") return;
|
|
122
|
+
const members = Array.isArray(typeAnn.types) ? typeAnn.types : [];
|
|
123
|
+
if (members.length === 0) return;
|
|
124
|
+
if (members.every(isStringLiteralTsType)) context.report({
|
|
125
|
+
node,
|
|
126
|
+
messageId: "preferEnum"
|
|
127
|
+
});
|
|
128
|
+
} };
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
var no_exported_string_union_types_default = rule$23;
|
|
132
|
+
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/utils/ast.ts
|
|
135
|
+
function isIdentifier$1(node, name) {
|
|
136
|
+
return node && node.type === "Identifier" && typeof node.name === "string" && (name === void 0 || node.name === name);
|
|
137
|
+
}
|
|
138
|
+
function isTemplateLiteralWithExpressions(node) {
|
|
139
|
+
return node && node.type === "TemplateLiteral" && Array.isArray(node.expressions) && node.expressions.length > 0;
|
|
140
|
+
}
|
|
141
|
+
function isStringConcat(node) {
|
|
142
|
+
return node && node.type === "BinaryExpression" && node.operator === "+";
|
|
143
|
+
}
|
|
144
|
+
function getFirstNonDirectiveStatement(statements) {
|
|
145
|
+
if (!Array.isArray(statements)) return null;
|
|
146
|
+
for (const stmt of statements) {
|
|
147
|
+
if (!stmt) continue;
|
|
148
|
+
if (stmt.type === "EmptyStatement") continue;
|
|
149
|
+
if (Object.prototype.hasOwnProperty.call(stmt, "directive")) continue;
|
|
150
|
+
return stmt;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
function findParentBlock(node) {
|
|
155
|
+
let current = node?.parent;
|
|
156
|
+
while (current && current.type !== "BlockStatement" && current.type !== "Program") current = current.parent;
|
|
157
|
+
return current || null;
|
|
158
|
+
}
|
|
159
|
+
function nodeContainsCall(node, predicate) {
|
|
160
|
+
const visited = /* @__PURE__ */ new WeakSet();
|
|
161
|
+
function walk(value) {
|
|
162
|
+
if (!value) return false;
|
|
163
|
+
if (predicate(value)) return true;
|
|
164
|
+
if (typeof value !== "object") return false;
|
|
165
|
+
if (visited.has(value)) return false;
|
|
166
|
+
visited.add(value);
|
|
167
|
+
if (Array.isArray(value)) return value.some(walk);
|
|
168
|
+
for (const [key, child] of Object.entries(value)) {
|
|
169
|
+
if (key === "parent") continue;
|
|
170
|
+
if (!child) continue;
|
|
171
|
+
if (typeof child !== "object") continue;
|
|
172
|
+
if (walk(child)) return true;
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return walk(node);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/rules/structured-logging.ts
|
|
181
|
+
const LOGGING_FUNCTIONS = new Set([
|
|
182
|
+
"logError",
|
|
183
|
+
"logException",
|
|
184
|
+
"logInfo",
|
|
185
|
+
"logWarn"
|
|
186
|
+
]);
|
|
187
|
+
function escapeSingleQuotes(text) {
|
|
188
|
+
return String(text).replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
189
|
+
}
|
|
190
|
+
function isDynamicString(node) {
|
|
191
|
+
return isTemplateLiteralWithExpressions(node) || isStringConcat(node);
|
|
192
|
+
}
|
|
193
|
+
function extractTemplateParts(node, sourceCode) {
|
|
194
|
+
const staticParts = [];
|
|
195
|
+
const expressions = [];
|
|
196
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
197
|
+
staticParts.push(node.quasis[i].value.raw);
|
|
198
|
+
if (i < node.expressions.length) {
|
|
199
|
+
const expr = node.expressions[i];
|
|
200
|
+
expressions.push({
|
|
201
|
+
node: expr,
|
|
202
|
+
text: sourceCode.getText(expr)
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const joined = staticParts.join("");
|
|
207
|
+
const isSimple = staticParts.length === 2;
|
|
208
|
+
const endsWithSingleSpace = joined.endsWith(" ") && !joined.endsWith(" ");
|
|
209
|
+
return {
|
|
210
|
+
message: isSimple && endsWithSingleSpace ? joined.trim() : joined,
|
|
211
|
+
expressions
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
function buildMetadataObject(expressions) {
|
|
215
|
+
if (!expressions.length) return "{ }";
|
|
216
|
+
return `{ ${expressions.map((expr, idx) => {
|
|
217
|
+
if (isIdentifier$1(expr.node)) return expr.node.name;
|
|
218
|
+
return `param${idx + 1}: ${expr.text}`;
|
|
219
|
+
}).join(", ")} }`;
|
|
220
|
+
}
|
|
221
|
+
const rule$22 = {
|
|
222
|
+
meta: {
|
|
223
|
+
type: "suggestion",
|
|
224
|
+
docs: { description: "Enforce structured logging message patterns." },
|
|
225
|
+
fixable: "code",
|
|
226
|
+
schema: [],
|
|
227
|
+
messages: { dynamicMessage: "Use a static message string and pass dynamic values as structured metadata (no template expressions or string concatenation)." }
|
|
228
|
+
},
|
|
229
|
+
create(context) {
|
|
230
|
+
const sourceCode = context.sourceCode;
|
|
231
|
+
return {
|
|
232
|
+
NewExpression(node) {
|
|
233
|
+
if (!isIdentifier$1(node.callee)) return;
|
|
234
|
+
const name = node.callee.name;
|
|
235
|
+
if (!name || !name.endsWith("Error")) return;
|
|
236
|
+
if (node.arguments.length !== 1) return;
|
|
237
|
+
const arg = node.arguments[0];
|
|
238
|
+
if (!isDynamicString(arg)) return;
|
|
239
|
+
context.report({
|
|
240
|
+
node,
|
|
241
|
+
messageId: "dynamicMessage",
|
|
242
|
+
fix(fixer) {
|
|
243
|
+
if (name !== "Error") return null;
|
|
244
|
+
if (!isTemplateLiteralWithExpressions(arg)) return null;
|
|
245
|
+
const { message, expressions } = extractTemplateParts(arg, sourceCode);
|
|
246
|
+
const meta = buildMetadataObject(expressions);
|
|
247
|
+
const replacement = `new MetaError('${escapeSingleQuotes(message)}', ${meta})`;
|
|
248
|
+
return fixer.replaceText(node, replacement);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
},
|
|
252
|
+
CallExpression(node) {
|
|
253
|
+
if (!isIdentifier$1(node.callee)) return;
|
|
254
|
+
const fnName = node.callee.name;
|
|
255
|
+
if (!LOGGING_FUNCTIONS.has(fnName)) return;
|
|
256
|
+
if (!(node.arguments || []).some(isDynamicString)) return;
|
|
257
|
+
context.report({
|
|
258
|
+
node,
|
|
259
|
+
messageId: "dynamicMessage",
|
|
260
|
+
fix(fixer) {
|
|
261
|
+
if (node.arguments.length !== 1) return null;
|
|
262
|
+
const arg = node.arguments[0];
|
|
263
|
+
if (!isTemplateLiteralWithExpressions(arg)) return null;
|
|
264
|
+
const { message, expressions } = extractTemplateParts(arg, sourceCode);
|
|
265
|
+
const meta = buildMetadataObject(expressions);
|
|
266
|
+
const replacement = `${fnName}('${escapeSingleQuotes(message)}', ${meta})`;
|
|
267
|
+
return fixer.replaceText(node, replacement);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
var structured_logging_default = rule$22;
|
|
275
|
+
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/utils/filename.ts
|
|
278
|
+
function getBasename(filename) {
|
|
279
|
+
return node_path.default.basename(filename || "");
|
|
280
|
+
}
|
|
281
|
+
function isTsOrTsx(filename) {
|
|
282
|
+
return typeof filename === "string" && (filename.endsWith(".ts") || filename.endsWith(".tsx"));
|
|
283
|
+
}
|
|
284
|
+
function isTs(filename) {
|
|
285
|
+
return typeof filename === "string" && filename.endsWith(".ts") && !filename.endsWith(".d.ts");
|
|
286
|
+
}
|
|
287
|
+
function isTsx(filename) {
|
|
288
|
+
return typeof filename === "string" && filename.endsWith(".tsx");
|
|
289
|
+
}
|
|
290
|
+
function isTestOrSpecFile(filename) {
|
|
291
|
+
const base = getBasename(filename);
|
|
292
|
+
return base.includes(".test.") || base.includes(".spec.") || filename.includes("__tests__");
|
|
293
|
+
}
|
|
294
|
+
function isTestSpecOrStoryFile(filename) {
|
|
295
|
+
const base = getBasename(filename);
|
|
296
|
+
return base.includes(".test.") || base.includes(".spec.") || base.includes(".stories.");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/utils/path.ts
|
|
301
|
+
function toPosixPath(filePath) {
|
|
302
|
+
if (typeof filePath !== "string") return "";
|
|
303
|
+
return filePath.replace(/\\/g, "/");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/rules/enum-file-organization.ts
|
|
308
|
+
function isExported$2(node) {
|
|
309
|
+
const parent = node?.parent;
|
|
310
|
+
return !!parent && (parent.type === "ExportNamedDeclaration" || parent.type === "ExportDefaultDeclaration");
|
|
311
|
+
}
|
|
312
|
+
const rule$21 = {
|
|
313
|
+
meta: {
|
|
314
|
+
type: "suggestion",
|
|
315
|
+
docs: { description: "Enforce that exported enums live in enums.ts." },
|
|
316
|
+
schema: [],
|
|
317
|
+
messages: {
|
|
318
|
+
wrongFile: "TypeScript enums must be defined in a file named \"enums.ts\".",
|
|
319
|
+
badEnumFile: "Files named \"enums.ts\" can only contain enum declarations and type exports (no imports).",
|
|
320
|
+
noImports: "Files named \"enums.ts\" cannot contain import statements."
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
create(context) {
|
|
324
|
+
const filename = toPosixPath(context.filename);
|
|
325
|
+
if (!isTsOrTsx(filename)) return {};
|
|
326
|
+
const isEnumsFile = filename.endsWith("/enums.ts") || filename === "enums.ts";
|
|
327
|
+
return {
|
|
328
|
+
TSEnumDeclaration(node) {
|
|
329
|
+
if (isExported$2(node) && !isEnumsFile) context.report({
|
|
330
|
+
node,
|
|
331
|
+
messageId: "wrongFile"
|
|
332
|
+
});
|
|
333
|
+
},
|
|
334
|
+
ImportDeclaration(node) {
|
|
335
|
+
if (isEnumsFile) context.report({
|
|
336
|
+
node,
|
|
337
|
+
messageId: "noImports"
|
|
338
|
+
});
|
|
339
|
+
},
|
|
340
|
+
Program(node) {
|
|
341
|
+
if (!isEnumsFile) return;
|
|
342
|
+
for (const stmt of node.body || []) {
|
|
343
|
+
const isEnum = stmt.type === "TSEnumDeclaration";
|
|
344
|
+
const isTypeAlias = stmt.type === "TSTypeAliasDeclaration";
|
|
345
|
+
const isInterface = stmt.type === "TSInterfaceDeclaration";
|
|
346
|
+
const isExportNamed = stmt.type === "ExportNamedDeclaration";
|
|
347
|
+
const exportDecl = isExportNamed ? stmt.declaration : null;
|
|
348
|
+
const isExportingTypesOrEnums = isExportNamed && (exportDecl === null || exportDecl?.type === "TSEnumDeclaration" || exportDecl?.type === "TSTypeAliasDeclaration" || exportDecl?.type === "TSInterfaceDeclaration");
|
|
349
|
+
const isReExportFromCurrentFile = isExportNamed && !stmt.source && Array.isArray(stmt.specifiers) && stmt.specifiers.length > 0;
|
|
350
|
+
if (stmt.type === "ImportDeclaration" || isEnum || isTypeAlias || isInterface || isExportingTypesOrEnums || isReExportFromCurrentFile) continue;
|
|
351
|
+
if (stmt.type !== "ExportAllDeclaration") context.report({
|
|
352
|
+
node: stmt,
|
|
353
|
+
messageId: "badEnumFile"
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
var enum_file_organization_default = rule$21;
|
|
361
|
+
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/utils/import.ts
|
|
364
|
+
function isTypeOnlyImport(node) {
|
|
365
|
+
if (!node || node.type !== "ImportDeclaration") return false;
|
|
366
|
+
if (node.importKind === "type") return true;
|
|
367
|
+
const specifiers = node.specifiers;
|
|
368
|
+
if (!Array.isArray(specifiers) || specifiers.length === 0) return false;
|
|
369
|
+
return specifiers.every((s) => s && s.importKind === "type");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/rules/types-file-organization.ts
|
|
374
|
+
function isExportedFromTypesModule(node) {
|
|
375
|
+
const parent = node?.parent;
|
|
376
|
+
if (!parent) return false;
|
|
377
|
+
if (parent.type === "ExportNamedDeclaration" || parent.type === "ExportDefaultDeclaration") return true;
|
|
378
|
+
if (parent.type === "TSModuleBlock" && parent.parent && parent.parent.parent && parent.parent.parent.type === "ExportNamedDeclaration") return true;
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
function isAllowedImport$2(importPath) {
|
|
382
|
+
if (typeof importPath !== "string") return false;
|
|
383
|
+
if (!importPath.startsWith(".") && !importPath.startsWith("/") && !importPath.startsWith("@/")) return true;
|
|
384
|
+
return importPath.endsWith("/enums") || importPath.endsWith("/enums.ts") || importPath === "enums" || importPath.endsWith("/types") || importPath.endsWith("/types.ts") || importPath === "types" || importPath.endsWith("/schema") || importPath.endsWith("/schema.ts") || importPath === "schema";
|
|
385
|
+
}
|
|
386
|
+
const rule$20 = {
|
|
387
|
+
meta: {
|
|
388
|
+
type: "suggestion",
|
|
389
|
+
docs: { description: "Enforce that exported types/interfaces live in types.ts." },
|
|
390
|
+
schema: [],
|
|
391
|
+
messages: {
|
|
392
|
+
wrongFile: "TypeScript types and interfaces must be defined in a file named \"types.ts\".",
|
|
393
|
+
badTypesFile: "Files named \"types.ts\" can only contain type aliases, interface declarations, and type exports.",
|
|
394
|
+
badImport: "Files named \"types.ts\" can only import external modules or internal enums/types/schema files."
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
create(context) {
|
|
398
|
+
const filename = toPosixPath(context.filename);
|
|
399
|
+
const isTypesFile = filename.endsWith("/types.ts") || filename === "types.ts";
|
|
400
|
+
return {
|
|
401
|
+
TSTypeAliasDeclaration(node) {
|
|
402
|
+
if (isExportedFromTypesModule(node) && !isTypesFile) context.report({
|
|
403
|
+
node,
|
|
404
|
+
messageId: "wrongFile"
|
|
405
|
+
});
|
|
406
|
+
},
|
|
407
|
+
TSInterfaceDeclaration(node) {
|
|
408
|
+
if (isExportedFromTypesModule(node) && !isTypesFile) context.report({
|
|
409
|
+
node,
|
|
410
|
+
messageId: "wrongFile"
|
|
411
|
+
});
|
|
412
|
+
},
|
|
413
|
+
ImportDeclaration(node) {
|
|
414
|
+
if (!isTypesFile) return;
|
|
415
|
+
if (isTypeOnlyImport(node)) return;
|
|
416
|
+
const importPath = node?.source?.value;
|
|
417
|
+
if (!isAllowedImport$2(importPath)) context.report({
|
|
418
|
+
node,
|
|
419
|
+
messageId: "badImport"
|
|
420
|
+
});
|
|
421
|
+
},
|
|
422
|
+
Program(node) {
|
|
423
|
+
if (!isTypesFile) return;
|
|
424
|
+
for (const stmt of node.body || []) {
|
|
425
|
+
const isTypeAlias = stmt.type === "TSTypeAliasDeclaration" || stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "TSTypeAliasDeclaration";
|
|
426
|
+
const isInterface = stmt.type === "TSInterfaceDeclaration" || stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "TSInterfaceDeclaration";
|
|
427
|
+
const isExportingTypes = stmt.type === "ExportNamedDeclaration" && (stmt.declaration === null || stmt.declaration?.type === "TSTypeAliasDeclaration" || stmt.declaration?.type === "TSInterfaceDeclaration");
|
|
428
|
+
const isReExportFromCurrentFile = stmt.type === "ExportNamedDeclaration" && !stmt.source && Array.isArray(stmt.specifiers) && stmt.specifiers.length > 0;
|
|
429
|
+
const isImport = stmt.type === "ImportDeclaration";
|
|
430
|
+
if (isTypeAlias || isInterface || isExportingTypes || isReExportFromCurrentFile || isImport) continue;
|
|
431
|
+
if (stmt.type !== "ExportAllDeclaration") context.report({
|
|
432
|
+
node: stmt,
|
|
433
|
+
messageId: "badTypesFile"
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
var types_file_organization_default = rule$20;
|
|
441
|
+
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/rules/constants-file-organization.ts
|
|
444
|
+
function isExported$1(node) {
|
|
445
|
+
const parent = node?.parent;
|
|
446
|
+
return !!parent && (parent.type === "ExportNamedDeclaration" || parent.type === "ExportDefaultDeclaration");
|
|
447
|
+
}
|
|
448
|
+
function isConstantsFile(filename) {
|
|
449
|
+
const f = toPosixPath(filename);
|
|
450
|
+
return f.endsWith("/constants.ts") || f.endsWith("/constants.tsx") || f === "constants.ts" || f === "constants.tsx";
|
|
451
|
+
}
|
|
452
|
+
function isAllowedImport$1(importPath) {
|
|
453
|
+
if (typeof importPath !== "string") return false;
|
|
454
|
+
if (!importPath.startsWith(".") && !importPath.startsWith("/") && !importPath.startsWith("@/")) return true;
|
|
455
|
+
if ([
|
|
456
|
+
".mp4",
|
|
457
|
+
".webm",
|
|
458
|
+
".ogg",
|
|
459
|
+
".mp3",
|
|
460
|
+
".wav",
|
|
461
|
+
".png",
|
|
462
|
+
".jpg",
|
|
463
|
+
".jpeg",
|
|
464
|
+
".gif",
|
|
465
|
+
".svg",
|
|
466
|
+
".webp"
|
|
467
|
+
].some((ext) => importPath.endsWith(ext))) return true;
|
|
468
|
+
return importPath.endsWith("/constants") || importPath.endsWith("/constants.ts") || importPath === "constants" || importPath.endsWith("/enums") || importPath.endsWith("/enums.ts") || importPath === "enums" || importPath.endsWith("/types") || importPath.endsWith("/types.ts") || importPath === "types" || importPath.endsWith("/schema") || importPath.endsWith("/schema.ts") || importPath === "schema";
|
|
469
|
+
}
|
|
470
|
+
function hasRuntimeInitializer(declarator) {
|
|
471
|
+
const init = declarator?.init;
|
|
472
|
+
return !!init && (init.type === "CallExpression" || init.type === "NewExpression" || init.type === "TaggedTemplateExpression");
|
|
473
|
+
}
|
|
474
|
+
const rule$19 = {
|
|
475
|
+
meta: {
|
|
476
|
+
type: "suggestion",
|
|
477
|
+
docs: { description: "Enforce constants file organization (constants.ts)." },
|
|
478
|
+
schema: [],
|
|
479
|
+
messages: {
|
|
480
|
+
wrongFile: "Exported constants must be defined in a file named \"constants.ts\".",
|
|
481
|
+
badConstantsFile: "Files named \"constants.ts\" can only contain const declarations.",
|
|
482
|
+
badImport: "Files named \"constants.ts\" can only import external modules, media files, or internal constants/enums/types/schema files."
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
create(context) {
|
|
486
|
+
const filename = context.filename;
|
|
487
|
+
const inConstantsFile = isConstantsFile(filename);
|
|
488
|
+
return {
|
|
489
|
+
VariableDeclaration(node) {
|
|
490
|
+
if (node.kind !== "const") return;
|
|
491
|
+
if (!isExported$1(node)) return;
|
|
492
|
+
if (inConstantsFile) return;
|
|
493
|
+
if (!(Array.isArray(node.declarations) ? node.declarations : []).some(hasRuntimeInitializer)) context.report({
|
|
494
|
+
node,
|
|
495
|
+
messageId: "wrongFile"
|
|
496
|
+
});
|
|
497
|
+
},
|
|
498
|
+
ImportDeclaration(node) {
|
|
499
|
+
if (!inConstantsFile) return;
|
|
500
|
+
const importPath = node?.source?.value;
|
|
501
|
+
if (!isAllowedImport$1(importPath)) context.report({
|
|
502
|
+
node,
|
|
503
|
+
messageId: "badImport"
|
|
504
|
+
});
|
|
505
|
+
},
|
|
506
|
+
Program(node) {
|
|
507
|
+
if (!inConstantsFile) return;
|
|
508
|
+
for (const stmt of node.body || []) {
|
|
509
|
+
const isImport = stmt.type === "ImportDeclaration";
|
|
510
|
+
const isTypeImport = stmt.type === "ImportDeclaration" && stmt.importKind === "type";
|
|
511
|
+
const isConstDecl = stmt.type === "VariableDeclaration" && stmt.kind === "const";
|
|
512
|
+
const isExportedConstDecl = stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "VariableDeclaration" && stmt.declaration.kind === "const";
|
|
513
|
+
const isReExport = stmt.type === "ExportNamedDeclaration" && !stmt.declaration && Array.isArray(stmt.specifiers) && stmt.specifiers.length > 0;
|
|
514
|
+
const isExportAllAllowed = stmt.type === "ExportAllDeclaration" && stmt.source && isAllowedImport$1(stmt.source.value);
|
|
515
|
+
if (isImport || isTypeImport || isConstDecl || isExportedConstDecl || isReExport || isExportAllAllowed) continue;
|
|
516
|
+
if (stmt.type !== "ExportAllDeclaration") context.report({
|
|
517
|
+
node: stmt,
|
|
518
|
+
messageId: "badConstantsFile"
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
var constants_file_organization_default = rule$19;
|
|
526
|
+
|
|
527
|
+
//#endregion
|
|
528
|
+
//#region src/rules/errors-file-organization.ts
|
|
529
|
+
function isErrorsFile(filename) {
|
|
530
|
+
const f = toPosixPath(filename);
|
|
531
|
+
return f.endsWith("/errors.ts") || f.endsWith("/errors.tsx") || f === "errors.ts" || f === "errors.tsx";
|
|
532
|
+
}
|
|
533
|
+
function isExported(node) {
|
|
534
|
+
const parent = node?.parent;
|
|
535
|
+
return !!parent && (parent.type === "ExportNamedDeclaration" || parent.type === "ExportDefaultDeclaration");
|
|
536
|
+
}
|
|
537
|
+
function extendsErrorClass(node) {
|
|
538
|
+
const superClass = node?.superClass;
|
|
539
|
+
if (!superClass) return false;
|
|
540
|
+
if (superClass.type === "Identifier") return superClass.name === "Error" || superClass.name.includes("Error");
|
|
541
|
+
if (superClass.type === "MemberExpression" && superClass.property?.type === "Identifier") {
|
|
542
|
+
const name = superClass.property.name;
|
|
543
|
+
return name === "Error" || name.includes("Error");
|
|
544
|
+
}
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
function isAllowedImport(importPath) {
|
|
548
|
+
if (typeof importPath !== "string") return false;
|
|
549
|
+
if (!importPath.startsWith(".") && !importPath.startsWith("/") && !importPath.startsWith("@/")) return true;
|
|
550
|
+
return importPath.endsWith("/enums") || importPath.endsWith("/enums.ts") || importPath === "enums" || importPath.endsWith("/types") || importPath.endsWith("/types.ts") || importPath === "types" || importPath.endsWith("/constants") || importPath.endsWith("/constants.ts") || importPath === "constants" || importPath.endsWith("/errors") || importPath.endsWith("/errors.ts") || importPath === "errors";
|
|
551
|
+
}
|
|
552
|
+
const rule$18 = {
|
|
553
|
+
meta: {
|
|
554
|
+
type: "suggestion",
|
|
555
|
+
docs: { description: "Enforce Error class organization (errors.ts)." },
|
|
556
|
+
schema: [],
|
|
557
|
+
messages: {
|
|
558
|
+
wrongFile: "Classes extending Error must be defined in a file named \"errors.ts\".",
|
|
559
|
+
badErrorsFile: "Files named \"errors.ts\" can only contain Error classes.",
|
|
560
|
+
badImport: "Files named \"errors.ts\" can only import external modules or internal enums/types/constants/errors files."
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
create(context) {
|
|
564
|
+
const filename = context.filename;
|
|
565
|
+
const inErrorsFile = isErrorsFile(filename);
|
|
566
|
+
return {
|
|
567
|
+
ClassDeclaration(node) {
|
|
568
|
+
if (!extendsErrorClass(node)) return;
|
|
569
|
+
if (!isExported(node)) return;
|
|
570
|
+
if (inErrorsFile) return;
|
|
571
|
+
context.report({
|
|
572
|
+
node,
|
|
573
|
+
messageId: "wrongFile"
|
|
574
|
+
});
|
|
575
|
+
},
|
|
576
|
+
ImportDeclaration(node) {
|
|
577
|
+
if (!inErrorsFile) return;
|
|
578
|
+
const importPath = node?.source?.value;
|
|
579
|
+
if (!isAllowedImport(importPath)) context.report({
|
|
580
|
+
node,
|
|
581
|
+
messageId: "badImport"
|
|
582
|
+
});
|
|
583
|
+
},
|
|
584
|
+
Program(node) {
|
|
585
|
+
if (!inErrorsFile) return;
|
|
586
|
+
for (const stmt of node.body || []) {
|
|
587
|
+
const isImport = stmt.type === "ImportDeclaration";
|
|
588
|
+
const isTypeImport = stmt.type === "ImportDeclaration" && stmt.importKind === "type";
|
|
589
|
+
const isErrorClass = stmt.type === "ClassDeclaration" && extendsErrorClass(stmt);
|
|
590
|
+
const isExportedErrorClass = stmt.type === "ExportNamedDeclaration" && stmt.declaration?.type === "ClassDeclaration" && extendsErrorClass(stmt.declaration);
|
|
591
|
+
const isReExport = stmt.type === "ExportNamedDeclaration" && !stmt.declaration && Array.isArray(stmt.specifiers) && stmt.specifiers.length > 0;
|
|
592
|
+
const isExportAllAllowed = stmt.type === "ExportAllDeclaration" && stmt.source && isAllowedImport(stmt.source.value);
|
|
593
|
+
if (isImport || isTypeImport || isErrorClass || isExportedErrorClass || isReExport || isExportAllAllowed) continue;
|
|
594
|
+
if (stmt.type !== "ExportAllDeclaration") context.report({
|
|
595
|
+
node: stmt,
|
|
596
|
+
messageId: "badErrorsFile"
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
var errors_file_organization_default = rule$18;
|
|
604
|
+
|
|
605
|
+
//#endregion
|
|
606
|
+
//#region src/rules/test-utils-organization.ts
|
|
607
|
+
const mockNamePatterns = [
|
|
608
|
+
/^mock[A-Z]/,
|
|
609
|
+
/^createMock[A-Z]/,
|
|
610
|
+
/^make(Mock|Test)[A-Z]/,
|
|
611
|
+
/^generate(Mock|Test)[A-Z]/,
|
|
612
|
+
/^fake[A-Z]/,
|
|
613
|
+
/^stub[A-Z]/,
|
|
614
|
+
/TestFactory$/,
|
|
615
|
+
/MockFactory$/
|
|
616
|
+
];
|
|
617
|
+
function isIdentifier(node) {
|
|
618
|
+
return node && node.type === "Identifier" && typeof node.name === "string";
|
|
619
|
+
}
|
|
620
|
+
const rule$17 = {
|
|
621
|
+
meta: {
|
|
622
|
+
type: "suggestion",
|
|
623
|
+
docs: { description: "Enforce that test utilities live under test-utils/." },
|
|
624
|
+
schema: [],
|
|
625
|
+
messages: {
|
|
626
|
+
wrongLocation: "Test utilities (mocks, fixtures, factories) should be defined in a test-utils directory.",
|
|
627
|
+
badMockName: "Mock functions should follow naming conventions (e.g., mockX, createMockX)."
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
create(context) {
|
|
631
|
+
const filename = toPosixPath(context.filename);
|
|
632
|
+
if (!isTsOrTsx(filename)) return {};
|
|
633
|
+
if (isTestOrSpecFile(filename)) return {};
|
|
634
|
+
const inTestUtils = filename.includes("/test-utils/");
|
|
635
|
+
return {
|
|
636
|
+
"FunctionDeclaration, VariableDeclaration"(node) {
|
|
637
|
+
if (inTestUtils) return;
|
|
638
|
+
if (node.type === "FunctionDeclaration" && isIdentifier(node.id)) {
|
|
639
|
+
const name = node.id.name;
|
|
640
|
+
if (mockNamePatterns.some((p) => p.test(name))) context.report({
|
|
641
|
+
node: node.id,
|
|
642
|
+
messageId: "wrongLocation"
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
if (node.type === "VariableDeclaration") {
|
|
646
|
+
for (const declarator of node.declarations || []) if (isIdentifier(declarator.id)) {
|
|
647
|
+
const name = declarator.id.name;
|
|
648
|
+
if (mockNamePatterns.some((p) => p.test(name))) context.report({
|
|
649
|
+
node: declarator.id,
|
|
650
|
+
messageId: "wrongLocation"
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
"VariableDeclarator, FunctionDeclaration"(node) {
|
|
656
|
+
if (!inTestUtils) return;
|
|
657
|
+
const name = node.type === "VariableDeclarator" ? node.id?.name : node.id?.name;
|
|
658
|
+
if (typeof name !== "string") return;
|
|
659
|
+
if (name.includes("mock") && !name.includes("Mock")) {
|
|
660
|
+
if (!mockNamePatterns.some((p) => p.test(name))) context.report({
|
|
661
|
+
node: node.id || node,
|
|
662
|
+
messageId: "badMockName"
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
var test_utils_organization_default = rule$17;
|
|
670
|
+
|
|
671
|
+
//#endregion
|
|
672
|
+
//#region src/rules/test-file-location.ts
|
|
673
|
+
const TEST_CALL_NAMES = new Set([
|
|
674
|
+
"describe",
|
|
675
|
+
"it",
|
|
676
|
+
"test",
|
|
677
|
+
"beforeEach",
|
|
678
|
+
"afterEach",
|
|
679
|
+
"beforeAll",
|
|
680
|
+
"afterAll",
|
|
681
|
+
"expect",
|
|
682
|
+
"jest",
|
|
683
|
+
"vitest",
|
|
684
|
+
"cy",
|
|
685
|
+
"assert",
|
|
686
|
+
"should",
|
|
687
|
+
"mock"
|
|
688
|
+
]);
|
|
689
|
+
function isTestCall(node) {
|
|
690
|
+
return node.type === "CallExpression" && node.callee && node.callee.type === "Identifier" && TEST_CALL_NAMES.has(node.callee.name);
|
|
691
|
+
}
|
|
692
|
+
const rule$16 = {
|
|
693
|
+
meta: {
|
|
694
|
+
type: "suggestion",
|
|
695
|
+
docs: { description: "Enforce colocated, non-empty test files." },
|
|
696
|
+
schema: [],
|
|
697
|
+
messages: {
|
|
698
|
+
badName: "Test file should follow naming convention: filename.test.ts",
|
|
699
|
+
badLocation: "Test files should be colocated with source files, not in a 'test' or '__tests__' directory.",
|
|
700
|
+
missingTests: "Test file should contain at least one test/describe block."
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
create(context) {
|
|
704
|
+
const filename = context.filename;
|
|
705
|
+
if (!isTsOrTsx(filename)) return {};
|
|
706
|
+
if (!filename.includes(".test.")) return {};
|
|
707
|
+
const dirname = node_path.default.dirname(filename);
|
|
708
|
+
const basename = node_path.default.basename(filename);
|
|
709
|
+
let hasTests = false;
|
|
710
|
+
return {
|
|
711
|
+
CallExpression(node) {
|
|
712
|
+
if (isTestCall(node)) hasTests = true;
|
|
713
|
+
},
|
|
714
|
+
"Program:exit"(node) {
|
|
715
|
+
if (!/^.+\.test\.(ts|tsx)$/.test(basename)) context.report({
|
|
716
|
+
node,
|
|
717
|
+
messageId: "badName"
|
|
718
|
+
});
|
|
719
|
+
if (dirname.split(node_path.default.sep).some((s) => s === "test" || s === "__tests__")) context.report({
|
|
720
|
+
node,
|
|
721
|
+
messageId: "badLocation"
|
|
722
|
+
});
|
|
723
|
+
if (!hasTests) context.report({
|
|
724
|
+
node,
|
|
725
|
+
messageId: "missingTests"
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
var test_file_location_default = rule$16;
|
|
732
|
+
|
|
733
|
+
//#endregion
|
|
734
|
+
//#region src/rules/require-test-files.ts
|
|
735
|
+
const DEFAULT_EXEMPT_BASENAMES = new Set([
|
|
736
|
+
"enums.ts",
|
|
737
|
+
"schema.ts",
|
|
738
|
+
"types.ts",
|
|
739
|
+
"constants.ts",
|
|
740
|
+
"errors.ts",
|
|
741
|
+
"index.ts",
|
|
742
|
+
"env.ts",
|
|
743
|
+
"config.ts",
|
|
744
|
+
"route.ts"
|
|
745
|
+
]);
|
|
746
|
+
const rule$15 = {
|
|
747
|
+
meta: {
|
|
748
|
+
type: "suggestion",
|
|
749
|
+
docs: { description: "Require a sibling *.test.ts for most *.ts files." },
|
|
750
|
+
schema: [],
|
|
751
|
+
messages: { missingTest: "TypeScript file \"{{filename}}\" is missing a corresponding test file \"{{testFilename}}\"." }
|
|
752
|
+
},
|
|
753
|
+
create(context) {
|
|
754
|
+
const filename = context.filename;
|
|
755
|
+
if (!isTs(filename)) return {};
|
|
756
|
+
const base = node_path.default.basename(filename);
|
|
757
|
+
if (filename.endsWith(".test.ts")) return {};
|
|
758
|
+
if (filename.endsWith(".config.ts")) return {};
|
|
759
|
+
if (filename.endsWith(".d.ts")) return {};
|
|
760
|
+
if (DEFAULT_EXEMPT_BASENAMES.has(base)) return {};
|
|
761
|
+
if (base.startsWith("use")) return {};
|
|
762
|
+
const dir = node_path.default.dirname(filename);
|
|
763
|
+
const testFilename = base.replace(/\.ts$/, ".test.ts");
|
|
764
|
+
const testPath = node_path.default.join(dir, testFilename);
|
|
765
|
+
return { "Program:exit"(node) {
|
|
766
|
+
if (!node_fs.default.existsSync(testPath)) context.report({
|
|
767
|
+
node,
|
|
768
|
+
messageId: "missingTest",
|
|
769
|
+
data: {
|
|
770
|
+
filename: base,
|
|
771
|
+
testFilename
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
} };
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
var require_test_files_default = rule$15;
|
|
778
|
+
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/rules/require-tsx-test-stories-files.ts
|
|
781
|
+
const EXEMPT_BASENAMES = new Set([
|
|
782
|
+
"index.ts",
|
|
783
|
+
"index.tsx",
|
|
784
|
+
"layout.tsx",
|
|
785
|
+
"page.tsx",
|
|
786
|
+
"loading.tsx",
|
|
787
|
+
"error.tsx",
|
|
788
|
+
"not-found.tsx",
|
|
789
|
+
"template.tsx",
|
|
790
|
+
"default.tsx",
|
|
791
|
+
"types.ts",
|
|
792
|
+
"types.tsx",
|
|
793
|
+
"constants.ts",
|
|
794
|
+
"constants.tsx",
|
|
795
|
+
"enums.ts",
|
|
796
|
+
"enums.tsx"
|
|
797
|
+
]);
|
|
798
|
+
const EXEMPT_PATTERNS = [
|
|
799
|
+
/^style-props\.(ts|tsx)$/,
|
|
800
|
+
/^config\.(ts|tsx)$/,
|
|
801
|
+
/^schema\.(ts|tsx)$/
|
|
802
|
+
];
|
|
803
|
+
const rule$14 = {
|
|
804
|
+
meta: {
|
|
805
|
+
type: "suggestion",
|
|
806
|
+
docs: { description: "Require sibling test files for TS/TSX, and sibling stories files for TSX." },
|
|
807
|
+
schema: [],
|
|
808
|
+
messages: {
|
|
809
|
+
missingTest: "{{fileType}} file \"{{filename}}\" is missing a corresponding test file \"{{testFilename}}\".",
|
|
810
|
+
missingStories: "TSX file \"{{filename}}\" is missing a corresponding stories file \"{{storiesFilename}}\"."
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
create(context) {
|
|
814
|
+
const filename = context.filename;
|
|
815
|
+
if (!isTsOrTsx(filename)) return {};
|
|
816
|
+
const base = node_path.default.basename(filename);
|
|
817
|
+
const dir = node_path.default.dirname(filename);
|
|
818
|
+
const tsx = isTsx(filename);
|
|
819
|
+
if (base.endsWith(".d.ts")) return {};
|
|
820
|
+
if (base.endsWith(".test.ts") || base.endsWith(".test.tsx")) return {};
|
|
821
|
+
if (base.endsWith(".stories.tsx")) return {};
|
|
822
|
+
if (EXEMPT_BASENAMES.has(base)) return {};
|
|
823
|
+
if (EXEMPT_PATTERNS.some((re) => re.test(base))) return {};
|
|
824
|
+
const ext = tsx ? ".tsx" : ".ts";
|
|
825
|
+
const testFilename = base.replace(/* @__PURE__ */ new RegExp(`\\${ext}$`), `.test${ext}`);
|
|
826
|
+
const testPath = node_path.default.join(dir, testFilename);
|
|
827
|
+
const storiesFilename = tsx ? base.replace(/\.tsx$/, ".stories.tsx") : null;
|
|
828
|
+
const storiesPath = tsx ? node_path.default.join(dir, storiesFilename) : null;
|
|
829
|
+
return { "Program:exit"(node) {
|
|
830
|
+
if (!node_fs.default.existsSync(testPath)) context.report({
|
|
831
|
+
node,
|
|
832
|
+
messageId: "missingTest",
|
|
833
|
+
data: {
|
|
834
|
+
fileType: tsx ? "TSX" : "TS",
|
|
835
|
+
filename: base,
|
|
836
|
+
testFilename
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
if (tsx && storiesPath && !node_fs.default.existsSync(storiesPath)) context.report({
|
|
840
|
+
node,
|
|
841
|
+
messageId: "missingStories",
|
|
842
|
+
data: {
|
|
843
|
+
filename: base,
|
|
844
|
+
storiesFilename
|
|
845
|
+
}
|
|
846
|
+
});
|
|
847
|
+
} };
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
var require_tsx_test_stories_files_default = rule$14;
|
|
851
|
+
|
|
852
|
+
//#endregion
|
|
853
|
+
//#region src/rules/vitest-mock-absolute-paths.ts
|
|
854
|
+
const VITEST_OBJECT_NAMES$1 = new Set(["vi", "vitest"]);
|
|
855
|
+
function isRelativePath(text) {
|
|
856
|
+
return typeof text === "string" && (text.startsWith(".") || text.startsWith(".."));
|
|
857
|
+
}
|
|
858
|
+
function isVitestMethodCall(node, names) {
|
|
859
|
+
if (!node || node.type !== "CallExpression") return false;
|
|
860
|
+
const callee = node.callee;
|
|
861
|
+
if (!callee || callee.type !== "MemberExpression") return false;
|
|
862
|
+
if (callee.object?.type !== "Identifier" || !VITEST_OBJECT_NAMES$1.has(callee.object.name)) return false;
|
|
863
|
+
if (callee.property?.type !== "Identifier") return false;
|
|
864
|
+
return names.includes(callee.property.name);
|
|
865
|
+
}
|
|
866
|
+
const rule$13 = {
|
|
867
|
+
meta: {
|
|
868
|
+
type: "problem",
|
|
869
|
+
docs: { description: "Require vi.mock() to use absolute (non-relative) module specifiers." },
|
|
870
|
+
schema: [],
|
|
871
|
+
messages: {
|
|
872
|
+
relative: "vi.mock() must use absolute paths. Use an absolute path instead of \"{{path}}\".",
|
|
873
|
+
requireResolve: "vi.mock() should not use require.resolve(). Use the absolute path directly."
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
create(context) {
|
|
877
|
+
const filename = context.filename;
|
|
878
|
+
if (!isTestOrSpecFile(filename)) return {};
|
|
879
|
+
const targetMethods = [
|
|
880
|
+
"mock",
|
|
881
|
+
"doMock",
|
|
882
|
+
"unmock",
|
|
883
|
+
"doUnmock"
|
|
884
|
+
];
|
|
885
|
+
return { CallExpression(node) {
|
|
886
|
+
if (!isVitestMethodCall(node, targetMethods)) return;
|
|
887
|
+
const firstArg = node.arguments?.[0];
|
|
888
|
+
if (!firstArg) return;
|
|
889
|
+
if (firstArg.type === "Literal" && typeof firstArg.value === "string") {
|
|
890
|
+
if (isRelativePath(firstArg.value)) context.report({
|
|
891
|
+
node: firstArg,
|
|
892
|
+
messageId: "relative",
|
|
893
|
+
data: { path: firstArg.value }
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
if (firstArg.type === "TemplateLiteral" && firstArg.quasis?.length) {
|
|
897
|
+
const cooked = firstArg.quasis[0]?.value?.cooked;
|
|
898
|
+
if (typeof cooked === "string" && isRelativePath(cooked)) context.report({
|
|
899
|
+
node: firstArg,
|
|
900
|
+
messageId: "relative",
|
|
901
|
+
data: { path: `${cooked}...` }
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
if (firstArg.type === "CallExpression" && firstArg.callee?.type === "MemberExpression" && firstArg.callee.object?.type === "Identifier" && firstArg.callee.object.name === "require" && firstArg.callee.property?.type === "Identifier" && firstArg.callee.property.name === "resolve") context.report({
|
|
905
|
+
node: firstArg,
|
|
906
|
+
messageId: "requireResolve"
|
|
907
|
+
});
|
|
908
|
+
} };
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
var vitest_mock_absolute_paths_default = rule$13;
|
|
912
|
+
|
|
913
|
+
//#endregion
|
|
914
|
+
//#region src/rules/vitest-mock-require-actual.ts
|
|
915
|
+
const VITEST_OBJECT_NAMES = new Set(["vi", "vitest"]);
|
|
916
|
+
function unwrapExpression(node) {
|
|
917
|
+
let current = node;
|
|
918
|
+
while (current && typeof current === "object") {
|
|
919
|
+
if (current.type === "TSAsExpression" || current.type === "TSTypeAssertion" || current.type === "TSNonNullExpression" || current.type === "ChainExpression" || current.type === "ParenthesizedExpression") {
|
|
920
|
+
current = current.expression;
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
break;
|
|
924
|
+
}
|
|
925
|
+
return current;
|
|
926
|
+
}
|
|
927
|
+
function isVitestMockCall(node) {
|
|
928
|
+
if (!node || node.type !== "CallExpression") return false;
|
|
929
|
+
const callee = node.callee;
|
|
930
|
+
if (!callee || callee.type !== "MemberExpression") return false;
|
|
931
|
+
if (callee.object?.type !== "Identifier" || !VITEST_OBJECT_NAMES.has(callee.object.name)) return false;
|
|
932
|
+
if (callee.property?.type !== "Identifier" || callee.property.name !== "mock") return false;
|
|
933
|
+
return true;
|
|
934
|
+
}
|
|
935
|
+
function isVitestImportActualCall(node) {
|
|
936
|
+
if (!node || node.type !== "CallExpression") return false;
|
|
937
|
+
const callee = node.callee;
|
|
938
|
+
if (!callee || callee.type !== "MemberExpression") return false;
|
|
939
|
+
if (callee.object?.type !== "Identifier" || !VITEST_OBJECT_NAMES.has(callee.object.name)) return false;
|
|
940
|
+
if (callee.property?.type !== "Identifier" || callee.property.name !== "importActual") return false;
|
|
941
|
+
return true;
|
|
942
|
+
}
|
|
943
|
+
function getFactoryFirstParamName(fnNode) {
|
|
944
|
+
if (!fnNode) return null;
|
|
945
|
+
if (fnNode.type !== "ArrowFunctionExpression" && fnNode.type !== "FunctionExpression") return null;
|
|
946
|
+
const param = fnNode.params?.[0];
|
|
947
|
+
if (!param) return null;
|
|
948
|
+
if (param.type !== "Identifier" || typeof param.name !== "string") return null;
|
|
949
|
+
return param.name;
|
|
950
|
+
}
|
|
951
|
+
function isCallToIdentifier(node, name) {
|
|
952
|
+
if (!node || node.type !== "CallExpression") return false;
|
|
953
|
+
const callee = node.callee;
|
|
954
|
+
return callee?.type === "Identifier" && callee.name === name;
|
|
955
|
+
}
|
|
956
|
+
function objectOnlySpreadsActual(objectExpression, importOriginalParamName) {
|
|
957
|
+
if (!objectExpression || objectExpression.type !== "ObjectExpression") return false;
|
|
958
|
+
const props = objectExpression.properties;
|
|
959
|
+
if (!Array.isArray(props) || props.length !== 1) return false;
|
|
960
|
+
const only = props[0];
|
|
961
|
+
const spreadArg = only.argument;
|
|
962
|
+
if (only.type !== "SpreadElement" && only.type !== "SpreadProperty" && only.type !== "ExperimentalSpreadProperty") return false;
|
|
963
|
+
const unwrapped = unwrapExpression(spreadArg);
|
|
964
|
+
const expr = unwrapped?.type === "AwaitExpression" ? unwrapExpression(unwrapped.argument) : unwrapped;
|
|
965
|
+
if (isVitestImportActualCall(expr)) return true;
|
|
966
|
+
if (importOriginalParamName && isCallToIdentifier(expr, importOriginalParamName)) return true;
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
function isPointlessActualOnly(fnNode) {
|
|
970
|
+
if (!fnNode) return false;
|
|
971
|
+
if (fnNode.type !== "ArrowFunctionExpression" && fnNode.type !== "FunctionExpression") return false;
|
|
972
|
+
const importOriginalParamName = getFactoryFirstParamName(fnNode);
|
|
973
|
+
const body = fnNode.body;
|
|
974
|
+
if (!body) return false;
|
|
975
|
+
if (body.type === "ObjectExpression") return objectOnlySpreadsActual(body, importOriginalParamName);
|
|
976
|
+
if (body.type === "BlockStatement") {
|
|
977
|
+
const statements = body.body || [];
|
|
978
|
+
if (statements.length !== 1) return false;
|
|
979
|
+
const stmt = statements[0];
|
|
980
|
+
if (stmt.type !== "ReturnStatement") return false;
|
|
981
|
+
return objectOnlySpreadsActual(stmt.argument, importOriginalParamName);
|
|
982
|
+
}
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
function findEnclosingExpressionStatement(node) {
|
|
986
|
+
let current = node;
|
|
987
|
+
while (current && current.parent) {
|
|
988
|
+
if (current.parent.type === "ExpressionStatement") return current.parent;
|
|
989
|
+
current = current.parent;
|
|
990
|
+
}
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
const rule$12 = {
|
|
994
|
+
meta: {
|
|
995
|
+
type: "problem",
|
|
996
|
+
docs: { description: "Require vi.mock() to include a factory which keeps non-mocked exports via vi.importActual() (or importOriginal)." },
|
|
997
|
+
fixable: "code",
|
|
998
|
+
schema: [],
|
|
999
|
+
messages: {
|
|
1000
|
+
missingSecondArg: "vi.mock() must have a second argument to prevent auto-mocking all exports.",
|
|
1001
|
+
missingImportActual: "The second argument of vi.mock() must contain a call to vi.importActual() (or call the factory's importOriginal parameter).",
|
|
1002
|
+
pointless: "vi.mock() that only spreads the original module is pointless and should be removed."
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
1005
|
+
create(context) {
|
|
1006
|
+
const filename = context.filename;
|
|
1007
|
+
if (!isTestOrSpecFile(filename)) return {};
|
|
1008
|
+
const sourceCode = context.sourceCode;
|
|
1009
|
+
return { CallExpression(node) {
|
|
1010
|
+
if (!isVitestMockCall(node)) return;
|
|
1011
|
+
const firstArg = node.arguments?.[0];
|
|
1012
|
+
if (!node.arguments || node.arguments.length < 2) {
|
|
1013
|
+
context.report({
|
|
1014
|
+
node,
|
|
1015
|
+
messageId: "missingSecondArg",
|
|
1016
|
+
fix(fixer) {
|
|
1017
|
+
if (!firstArg) return null;
|
|
1018
|
+
if (firstArg.type !== "Literal" || typeof firstArg.value !== "string") return null;
|
|
1019
|
+
const modulePathText = sourceCode.getText(firstArg);
|
|
1020
|
+
return fixer.insertTextAfter(firstArg, `, async () => ({ ...(await vi.importActual(${modulePathText})) })`);
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const secondArg = node.arguments[1];
|
|
1026
|
+
const importOriginalParamName = getFactoryFirstParamName(secondArg);
|
|
1027
|
+
const hasImportActual = nodeContainsCall(secondArg, isVitestImportActualCall);
|
|
1028
|
+
const hasImportOriginalCall = importOriginalParamName ? nodeContainsCall(secondArg, (n) => isCallToIdentifier(n, importOriginalParamName)) : false;
|
|
1029
|
+
if (!hasImportActual && !hasImportOriginalCall) {
|
|
1030
|
+
context.report({
|
|
1031
|
+
node: secondArg || node,
|
|
1032
|
+
messageId: "missingImportActual"
|
|
1033
|
+
});
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (isPointlessActualOnly(secondArg)) context.report({
|
|
1037
|
+
node: secondArg,
|
|
1038
|
+
messageId: "pointless",
|
|
1039
|
+
fix(fixer) {
|
|
1040
|
+
const stmt = findEnclosingExpressionStatement(node);
|
|
1041
|
+
if (!stmt) return null;
|
|
1042
|
+
return fixer.remove(stmt);
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
} };
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
var vitest_mock_require_actual_default = rule$12;
|
|
1049
|
+
|
|
1050
|
+
//#endregion
|
|
1051
|
+
//#region src/rules/vitest-no-focused-tests.ts
|
|
1052
|
+
const TEST_API_ROOT_NAMES$1 = new Set([
|
|
1053
|
+
"describe",
|
|
1054
|
+
"it",
|
|
1055
|
+
"test",
|
|
1056
|
+
"suite"
|
|
1057
|
+
]);
|
|
1058
|
+
function unwrapCallee$1(node) {
|
|
1059
|
+
let current = node;
|
|
1060
|
+
while (current && typeof current === "object") {
|
|
1061
|
+
if (current.type === "ChainExpression") {
|
|
1062
|
+
current = current.expression;
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
return current;
|
|
1068
|
+
}
|
|
1069
|
+
function getMemberChain$1(callee) {
|
|
1070
|
+
const members = [];
|
|
1071
|
+
let current = unwrapCallee$1(callee);
|
|
1072
|
+
while (current && typeof current === "object") {
|
|
1073
|
+
if (current.type === "MemberExpression") {
|
|
1074
|
+
if (current.computed) return {
|
|
1075
|
+
root: null,
|
|
1076
|
+
members
|
|
1077
|
+
};
|
|
1078
|
+
if (current.property?.type !== "Identifier") return {
|
|
1079
|
+
root: null,
|
|
1080
|
+
members
|
|
1081
|
+
};
|
|
1082
|
+
members.push(current.property.name);
|
|
1083
|
+
current = unwrapCallee$1(current.object);
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
if (current.type === "Identifier") return {
|
|
1087
|
+
root: current.name,
|
|
1088
|
+
members
|
|
1089
|
+
};
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
return {
|
|
1093
|
+
root: null,
|
|
1094
|
+
members
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
const rule$11 = {
|
|
1098
|
+
meta: {
|
|
1099
|
+
type: "problem",
|
|
1100
|
+
docs: { description: "Disallow focused Vitest tests (describe.only / it.only / test.only)." },
|
|
1101
|
+
schema: [],
|
|
1102
|
+
messages: { focused: "Focused tests are not allowed (remove .only before committing)." }
|
|
1103
|
+
},
|
|
1104
|
+
create(context) {
|
|
1105
|
+
const filename = context.filename;
|
|
1106
|
+
if (!isTestOrSpecFile(filename)) return {};
|
|
1107
|
+
return { CallExpression(node) {
|
|
1108
|
+
const { root, members } = getMemberChain$1(node.callee);
|
|
1109
|
+
if (!root || !TEST_API_ROOT_NAMES$1.has(root)) return;
|
|
1110
|
+
if (!members.includes("only")) return;
|
|
1111
|
+
context.report({
|
|
1112
|
+
node: node.callee || node,
|
|
1113
|
+
messageId: "focused"
|
|
1114
|
+
});
|
|
1115
|
+
} };
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
var vitest_no_focused_tests_default = rule$11;
|
|
1119
|
+
|
|
1120
|
+
//#endregion
|
|
1121
|
+
//#region src/rules/vitest-no-disabled-tests.ts
|
|
1122
|
+
const TEST_API_ROOT_NAMES = new Set([
|
|
1123
|
+
"describe",
|
|
1124
|
+
"it",
|
|
1125
|
+
"test",
|
|
1126
|
+
"suite"
|
|
1127
|
+
]);
|
|
1128
|
+
const DISABLED_MEMBERS = new Set(["skip", "todo"]);
|
|
1129
|
+
function unwrapCallee(node) {
|
|
1130
|
+
let current = node;
|
|
1131
|
+
while (current && typeof current === "object") {
|
|
1132
|
+
if (current.type === "ChainExpression") {
|
|
1133
|
+
current = current.expression;
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
break;
|
|
1137
|
+
}
|
|
1138
|
+
return current;
|
|
1139
|
+
}
|
|
1140
|
+
function getMemberChain(callee) {
|
|
1141
|
+
const members = [];
|
|
1142
|
+
let current = unwrapCallee(callee);
|
|
1143
|
+
while (current && typeof current === "object") {
|
|
1144
|
+
if (current.type === "MemberExpression") {
|
|
1145
|
+
if (current.computed) return {
|
|
1146
|
+
root: null,
|
|
1147
|
+
members
|
|
1148
|
+
};
|
|
1149
|
+
if (current.property?.type !== "Identifier") return {
|
|
1150
|
+
root: null,
|
|
1151
|
+
members
|
|
1152
|
+
};
|
|
1153
|
+
members.push(current.property.name);
|
|
1154
|
+
current = unwrapCallee(current.object);
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
if (current.type === "Identifier") return {
|
|
1158
|
+
root: current.name,
|
|
1159
|
+
members
|
|
1160
|
+
};
|
|
1161
|
+
break;
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
root: null,
|
|
1165
|
+
members
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
const rule$10 = {
|
|
1169
|
+
meta: {
|
|
1170
|
+
type: "problem",
|
|
1171
|
+
docs: { description: "Disallow disabled Vitest tests (describe.skip / it.skip / test.skip / *.todo)." },
|
|
1172
|
+
schema: [],
|
|
1173
|
+
messages: { disabled: "Disabled tests are not allowed (remove .skip/.todo before committing)." }
|
|
1174
|
+
},
|
|
1175
|
+
create(context) {
|
|
1176
|
+
const filename = context.filename;
|
|
1177
|
+
if (!isTestOrSpecFile(filename)) return {};
|
|
1178
|
+
return { CallExpression(node) {
|
|
1179
|
+
const { root, members } = getMemberChain(node.callee);
|
|
1180
|
+
if (!root || !TEST_API_ROOT_NAMES.has(root)) return;
|
|
1181
|
+
if (!members.some((m) => DISABLED_MEMBERS.has(m))) return;
|
|
1182
|
+
context.report({
|
|
1183
|
+
node: node.callee || node,
|
|
1184
|
+
messageId: "disabled"
|
|
1185
|
+
});
|
|
1186
|
+
} };
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
var vitest_no_disabled_tests_default = rule$10;
|
|
1190
|
+
|
|
1191
|
+
//#endregion
|
|
1192
|
+
//#region src/rules/vitest-no-test-prefixes.ts
|
|
1193
|
+
const BANNED_PREFIX_CALLS = new Set([
|
|
1194
|
+
"fdescribe",
|
|
1195
|
+
"fit",
|
|
1196
|
+
"fcontext",
|
|
1197
|
+
"xdescribe",
|
|
1198
|
+
"xit",
|
|
1199
|
+
"xtest",
|
|
1200
|
+
"xcontext"
|
|
1201
|
+
]);
|
|
1202
|
+
const rule$9 = {
|
|
1203
|
+
meta: {
|
|
1204
|
+
type: "problem",
|
|
1205
|
+
docs: { description: "Disallow fdescribe/fit/xdescribe/xit/xtest style test prefixes in Vitest tests." },
|
|
1206
|
+
schema: [],
|
|
1207
|
+
messages: { banned: "Test prefixes like fdescribe/fit/xdescribe/xit/xtest are not allowed." }
|
|
1208
|
+
},
|
|
1209
|
+
create(context) {
|
|
1210
|
+
const filename = context.filename;
|
|
1211
|
+
if (!isTestOrSpecFile(filename)) return {};
|
|
1212
|
+
return { CallExpression(node) {
|
|
1213
|
+
const callee = node.callee;
|
|
1214
|
+
if (!callee || callee.type !== "Identifier") return;
|
|
1215
|
+
if (!BANNED_PREFIX_CALLS.has(callee.name)) return;
|
|
1216
|
+
context.report({
|
|
1217
|
+
node: callee,
|
|
1218
|
+
messageId: "banned"
|
|
1219
|
+
});
|
|
1220
|
+
} };
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
var vitest_no_test_prefixes_default = rule$9;
|
|
1224
|
+
|
|
1225
|
+
//#endregion
|
|
1226
|
+
//#region src/rules/no-log-exception-with-throw.ts
|
|
1227
|
+
function isLogCall(node) {
|
|
1228
|
+
return node.type === "CallExpression" && node.callee && node.callee.type === "Identifier" && (node.callee.name === "logException" || node.callee.name === "logError");
|
|
1229
|
+
}
|
|
1230
|
+
const rule$8 = {
|
|
1231
|
+
meta: {
|
|
1232
|
+
type: "problem",
|
|
1233
|
+
docs: { description: "Disallow logException()/logError() in the same block as a throw statement." },
|
|
1234
|
+
schema: [],
|
|
1235
|
+
messages: { duplicate: "Do not log and throw in the same block. It causes duplicate error reporting; choose one." }
|
|
1236
|
+
},
|
|
1237
|
+
create(context) {
|
|
1238
|
+
const logCalls = [];
|
|
1239
|
+
const throwBlocks = /* @__PURE__ */ new Set();
|
|
1240
|
+
return {
|
|
1241
|
+
CallExpression(node) {
|
|
1242
|
+
if (!isLogCall(node)) return;
|
|
1243
|
+
logCalls.push({
|
|
1244
|
+
node,
|
|
1245
|
+
block: findParentBlock(node)
|
|
1246
|
+
});
|
|
1247
|
+
},
|
|
1248
|
+
ThrowStatement(node) {
|
|
1249
|
+
const block = findParentBlock(node);
|
|
1250
|
+
if (block) throwBlocks.add(block);
|
|
1251
|
+
},
|
|
1252
|
+
"Program:exit"() {
|
|
1253
|
+
for (const log of logCalls) if (log.block && throwBlocks.has(log.block)) context.report({
|
|
1254
|
+
node: log.node,
|
|
1255
|
+
messageId: "duplicate"
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
var no_log_exception_with_throw_default = rule$8;
|
|
1262
|
+
|
|
1263
|
+
//#endregion
|
|
1264
|
+
//#region src/rules/require-route-middleware.ts
|
|
1265
|
+
const HTTP_METHODS = new Set([
|
|
1266
|
+
"GET",
|
|
1267
|
+
"POST",
|
|
1268
|
+
"PUT",
|
|
1269
|
+
"DELETE",
|
|
1270
|
+
"PATCH",
|
|
1271
|
+
"OPTIONS",
|
|
1272
|
+
"HEAD"
|
|
1273
|
+
]);
|
|
1274
|
+
function isHandleMiddlewareCallee(callee) {
|
|
1275
|
+
if (!callee) return false;
|
|
1276
|
+
if (callee.type === "Identifier") return callee.name.startsWith("handle") && callee.name.endsWith("Middleware");
|
|
1277
|
+
if (callee.type === "MemberExpression" && callee.property?.type === "Identifier") {
|
|
1278
|
+
const name = callee.property.name;
|
|
1279
|
+
return name.startsWith("handle") && name.endsWith("Middleware");
|
|
1280
|
+
}
|
|
1281
|
+
return false;
|
|
1282
|
+
}
|
|
1283
|
+
function expressionIsMiddlewareCall(expr) {
|
|
1284
|
+
if (!expr) return false;
|
|
1285
|
+
if (expr.type === "CallExpression") return isHandleMiddlewareCallee(expr.callee);
|
|
1286
|
+
if (expr.type === "AwaitExpression") {
|
|
1287
|
+
const arg = expr.argument;
|
|
1288
|
+
return arg?.type === "CallExpression" && isHandleMiddlewareCallee(arg.callee);
|
|
1289
|
+
}
|
|
1290
|
+
return false;
|
|
1291
|
+
}
|
|
1292
|
+
function functionFirstStatementCallsMiddleware(fnNode) {
|
|
1293
|
+
if (!fnNode) return false;
|
|
1294
|
+
if (fnNode.type === "ArrowFunctionExpression" && fnNode.body?.type !== "BlockStatement") return expressionIsMiddlewareCall(fnNode.body);
|
|
1295
|
+
const first = getFirstNonDirectiveStatement(fnNode.body?.type === "BlockStatement" ? fnNode.body.body : []);
|
|
1296
|
+
if (!first) return false;
|
|
1297
|
+
if (first.type === "ReturnStatement") return expressionIsMiddlewareCall(first.argument);
|
|
1298
|
+
if (first.type === "ExpressionStatement") return expressionIsMiddlewareCall(first.expression);
|
|
1299
|
+
if (first.type === "VariableDeclaration") for (const decl of first.declarations || []) {
|
|
1300
|
+
const init = decl?.init;
|
|
1301
|
+
if (!init) continue;
|
|
1302
|
+
if (expressionIsMiddlewareCall(init)) return true;
|
|
1303
|
+
}
|
|
1304
|
+
return false;
|
|
1305
|
+
}
|
|
1306
|
+
function isFunctionLike(node) {
|
|
1307
|
+
return node && (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression");
|
|
1308
|
+
}
|
|
1309
|
+
const rule$7 = {
|
|
1310
|
+
meta: {
|
|
1311
|
+
type: "problem",
|
|
1312
|
+
docs: { description: "Require route.ts exported HTTP handlers to call handle*Middleware as the first statement." },
|
|
1313
|
+
schema: [],
|
|
1314
|
+
messages: { missing: "The first statement of each exported route handler must be a call to a handle*Middleware function (e.g., handleRouteMiddleware)." }
|
|
1315
|
+
},
|
|
1316
|
+
create(context) {
|
|
1317
|
+
const filename = toPosixPath(context.filename);
|
|
1318
|
+
if (!filename.endsWith("route.ts")) return {};
|
|
1319
|
+
if (filename.includes("/api/v0/")) return {};
|
|
1320
|
+
const handlers = [];
|
|
1321
|
+
return {
|
|
1322
|
+
ExportNamedDeclaration(node) {
|
|
1323
|
+
if (node.declaration?.type === "FunctionDeclaration") {
|
|
1324
|
+
const fn = node.declaration;
|
|
1325
|
+
if (fn.id?.type === "Identifier" && HTTP_METHODS.has(fn.id.name)) handlers.push(fn);
|
|
1326
|
+
}
|
|
1327
|
+
if (node.declaration?.type === "VariableDeclaration") for (const declarator of node.declaration.declarations || []) {
|
|
1328
|
+
if (declarator.id?.type !== "Identifier") continue;
|
|
1329
|
+
if (!HTTP_METHODS.has(declarator.id.name)) continue;
|
|
1330
|
+
if (isFunctionLike(declarator.init)) handlers.push(declarator.init);
|
|
1331
|
+
}
|
|
1332
|
+
},
|
|
1333
|
+
"Program:exit"() {
|
|
1334
|
+
for (const fn of handlers) if (!functionFirstStatementCallsMiddleware(fn)) context.report({
|
|
1335
|
+
node: fn,
|
|
1336
|
+
messageId: "missing"
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
var require_route_middleware_default = rule$7;
|
|
1343
|
+
|
|
1344
|
+
//#endregion
|
|
1345
|
+
//#region src/rules/require-v0-route-handle-middleware.ts
|
|
1346
|
+
function isHandleV0MiddlewareCall(expr) {
|
|
1347
|
+
return expr && expr.type === "CallExpression" && expr.callee?.type === "Identifier" && expr.callee.name === "handleV0RouteMiddleware";
|
|
1348
|
+
}
|
|
1349
|
+
function handlerStartsWithV0Middleware(fnNode) {
|
|
1350
|
+
if (!fnNode) return false;
|
|
1351
|
+
if (fnNode.type === "ArrowFunctionExpression" && fnNode.body?.type !== "BlockStatement") return isHandleV0MiddlewareCall(fnNode.body);
|
|
1352
|
+
const first = getFirstNonDirectiveStatement(fnNode.body?.type === "BlockStatement" ? fnNode.body.body : []);
|
|
1353
|
+
if (!first) return false;
|
|
1354
|
+
if (first.type === "ReturnStatement") return isHandleV0MiddlewareCall(first.argument);
|
|
1355
|
+
if (first.type === "ExpressionStatement") return isHandleV0MiddlewareCall(first.expression);
|
|
1356
|
+
return false;
|
|
1357
|
+
}
|
|
1358
|
+
const rule$6 = {
|
|
1359
|
+
meta: {
|
|
1360
|
+
type: "problem",
|
|
1361
|
+
docs: { description: "Require v0 API route handlers to call handleV0RouteMiddleware inside .handler(...)." },
|
|
1362
|
+
schema: [],
|
|
1363
|
+
messages: { missing: "v0 API route handlers must call handleV0RouteMiddleware. Usage: .handler(async (req) => handleV0RouteMiddleware(req, async (user) => { ... }))" }
|
|
1364
|
+
},
|
|
1365
|
+
create(context) {
|
|
1366
|
+
const filename = toPosixPath(context.filename);
|
|
1367
|
+
if (!filename.includes("/api/v0/") || !filename.endsWith("route.ts")) return {};
|
|
1368
|
+
const handlerCalls = [];
|
|
1369
|
+
return {
|
|
1370
|
+
CallExpression(node) {
|
|
1371
|
+
const callee = node.callee;
|
|
1372
|
+
if (callee?.type !== "MemberExpression") return;
|
|
1373
|
+
if (callee.property?.type !== "Identifier" || callee.property.name !== "handler") return;
|
|
1374
|
+
const arg = node.arguments?.[0];
|
|
1375
|
+
if (!arg) return;
|
|
1376
|
+
if (arg.type !== "ArrowFunctionExpression" && arg.type !== "FunctionExpression") return;
|
|
1377
|
+
handlerCalls.push({
|
|
1378
|
+
call: node,
|
|
1379
|
+
fn: arg
|
|
1380
|
+
});
|
|
1381
|
+
},
|
|
1382
|
+
"Program:exit"() {
|
|
1383
|
+
for (const { call, fn } of handlerCalls) if (!handlerStartsWithV0Middleware(fn)) context.report({
|
|
1384
|
+
node: call,
|
|
1385
|
+
messageId: "missing"
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
};
|
|
1391
|
+
var require_v0_route_handle_middleware_default = rule$6;
|
|
1392
|
+
|
|
1393
|
+
//#endregion
|
|
1394
|
+
//#region src/rules/require-v0-strict-schemas.ts
|
|
1395
|
+
function isZodObjectCall(node) {
|
|
1396
|
+
return node && node.type === "CallExpression" && node.callee?.type === "MemberExpression" && node.callee.object?.type === "Identifier" && node.callee.object.name === "z" && node.callee.property?.type === "Identifier" && node.callee.property.name === "object";
|
|
1397
|
+
}
|
|
1398
|
+
function isExtendCall(node) {
|
|
1399
|
+
return node && node.type === "CallExpression" && node.callee?.type === "MemberExpression" && node.callee.property?.type === "Identifier" && node.callee.property.name === "extend";
|
|
1400
|
+
}
|
|
1401
|
+
function hasStrictInChain(node) {
|
|
1402
|
+
let current = node;
|
|
1403
|
+
let parent = current.parent;
|
|
1404
|
+
while (parent) {
|
|
1405
|
+
if (parent.type === "MemberExpression" && parent.object === current) {
|
|
1406
|
+
if (parent.property?.type === "Identifier" && parent.property.name === "strict") return true;
|
|
1407
|
+
current = parent;
|
|
1408
|
+
parent = current.parent;
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
if (parent.type === "CallExpression" && parent.callee === current) {
|
|
1412
|
+
current = parent;
|
|
1413
|
+
parent = current.parent;
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1418
|
+
return false;
|
|
1419
|
+
}
|
|
1420
|
+
function findChainEnd(node) {
|
|
1421
|
+
let current = node;
|
|
1422
|
+
let parent = current.parent;
|
|
1423
|
+
while (parent) {
|
|
1424
|
+
if (parent.type === "CallExpression" && parent.callee?.type === "MemberExpression" && parent.callee.object === current) {
|
|
1425
|
+
current = parent;
|
|
1426
|
+
parent = current.parent;
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
if (parent.type === "MemberExpression" && parent.object === current) {
|
|
1430
|
+
current = parent;
|
|
1431
|
+
parent = current.parent;
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
break;
|
|
1435
|
+
}
|
|
1436
|
+
return current;
|
|
1437
|
+
}
|
|
1438
|
+
const rule$5 = {
|
|
1439
|
+
meta: {
|
|
1440
|
+
type: "problem",
|
|
1441
|
+
docs: { description: "Require .strict() for v0 API Zod schemas." },
|
|
1442
|
+
fixable: "code",
|
|
1443
|
+
schema: [],
|
|
1444
|
+
messages: { missing: "v0 API schemas must use .strict() on z.object() to reject unknown properties. Add .strict() after the object definition." }
|
|
1445
|
+
},
|
|
1446
|
+
create(context) {
|
|
1447
|
+
const filename = toPosixPath(context.filename);
|
|
1448
|
+
if (!filename.includes("/api/v0/")) return {};
|
|
1449
|
+
const isSchemaFile = filename.includes("/schemas/");
|
|
1450
|
+
const isRouteFile = filename.endsWith("route.ts");
|
|
1451
|
+
if (!isSchemaFile && !isRouteFile) return {};
|
|
1452
|
+
return { CallExpression(node) {
|
|
1453
|
+
if (!isZodObjectCall(node) && !isExtendCall(node)) return;
|
|
1454
|
+
if (hasStrictInChain(node)) return;
|
|
1455
|
+
const end = findChainEnd(node);
|
|
1456
|
+
context.report({
|
|
1457
|
+
node: end,
|
|
1458
|
+
messageId: "missing",
|
|
1459
|
+
fix(fixer) {
|
|
1460
|
+
return fixer.insertTextAfter(end, ".strict()");
|
|
1461
|
+
}
|
|
1462
|
+
});
|
|
1463
|
+
} };
|
|
1464
|
+
}
|
|
1465
|
+
};
|
|
1466
|
+
var require_v0_strict_schemas_default = rule$5;
|
|
1467
|
+
|
|
1468
|
+
//#endregion
|
|
1469
|
+
//#region src/rules/filename-match-export.ts
|
|
1470
|
+
function isUppercaseComponentName(name) {
|
|
1471
|
+
return typeof name === "string" && name[0] === name[0]?.toUpperCase();
|
|
1472
|
+
}
|
|
1473
|
+
function isReactComponentExport(node) {
|
|
1474
|
+
if (node.type === "FunctionDeclaration" && node.id) return isUppercaseComponentName(node.id.name);
|
|
1475
|
+
if (node.type === "VariableDeclaration") for (const declarator of node.declarations || []) {
|
|
1476
|
+
const id = declarator.id;
|
|
1477
|
+
const init = declarator.init;
|
|
1478
|
+
if (id?.type !== "Identifier") continue;
|
|
1479
|
+
if (!isUppercaseComponentName(id.name)) continue;
|
|
1480
|
+
if (!init) continue;
|
|
1481
|
+
if (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression") return true;
|
|
1482
|
+
}
|
|
1483
|
+
return false;
|
|
1484
|
+
}
|
|
1485
|
+
function getExportedName(node) {
|
|
1486
|
+
if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "FunctionDeclaration" && node.declaration.id) return node.declaration.id.name;
|
|
1487
|
+
if (node.type === "ExportNamedDeclaration" && node.declaration?.type === "VariableDeclaration") {
|
|
1488
|
+
const decl = node.declaration.declarations?.[0];
|
|
1489
|
+
if (decl?.id?.type === "Identifier") return decl.id.name;
|
|
1490
|
+
}
|
|
1491
|
+
if (node.type === "ExportDefaultDeclaration" && node.declaration?.type === "FunctionDeclaration" && node.declaration.id) return node.declaration.id.name;
|
|
1492
|
+
if (node.type === "ExportDefaultDeclaration" && node.declaration?.type === "Identifier") return node.declaration.name;
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
function stripExt(filename) {
|
|
1496
|
+
const base = node_path.default.basename(filename);
|
|
1497
|
+
const ext = node_path.default.extname(base);
|
|
1498
|
+
return base.slice(0, -ext.length).replace(/\.(tsx|ts|jsx|js)$/, "");
|
|
1499
|
+
}
|
|
1500
|
+
function suffixExpectationFromFilename(base) {
|
|
1501
|
+
for (const p of [
|
|
1502
|
+
{
|
|
1503
|
+
key: "page",
|
|
1504
|
+
re: /^(.+)\.page\.(tsx|ts|jsx|js)$/i,
|
|
1505
|
+
suffix: "Page"
|
|
1506
|
+
},
|
|
1507
|
+
{
|
|
1508
|
+
key: "module",
|
|
1509
|
+
re: /^(.+)\.module\.(tsx|ts|jsx|js)$/i,
|
|
1510
|
+
suffix: "Module"
|
|
1511
|
+
},
|
|
1512
|
+
{
|
|
1513
|
+
key: "layout",
|
|
1514
|
+
re: /^(.+)\.layout\.(tsx|ts|jsx|js)$/i,
|
|
1515
|
+
suffix: "Layout"
|
|
1516
|
+
},
|
|
1517
|
+
{
|
|
1518
|
+
key: "view",
|
|
1519
|
+
re: /^(.+)\.view\.(tsx|ts|jsx|js)$/i,
|
|
1520
|
+
suffix: "View"
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
key: "provider",
|
|
1524
|
+
re: /^(.+)\.provider\.(tsx|ts|jsx|js)$/i,
|
|
1525
|
+
suffix: "Provider"
|
|
1526
|
+
}
|
|
1527
|
+
]) {
|
|
1528
|
+
const m = base.match(p.re);
|
|
1529
|
+
if (m) return {
|
|
1530
|
+
baseName: m[1],
|
|
1531
|
+
suffix: p.suffix
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
return null;
|
|
1535
|
+
}
|
|
1536
|
+
function suggestFilenameFromExport(exportName, ext) {
|
|
1537
|
+
for (const s of [
|
|
1538
|
+
{
|
|
1539
|
+
suffix: "Page",
|
|
1540
|
+
file: "page"
|
|
1541
|
+
},
|
|
1542
|
+
{
|
|
1543
|
+
suffix: "Module",
|
|
1544
|
+
file: "module"
|
|
1545
|
+
},
|
|
1546
|
+
{
|
|
1547
|
+
suffix: "Layout",
|
|
1548
|
+
file: "layout"
|
|
1549
|
+
},
|
|
1550
|
+
{
|
|
1551
|
+
suffix: "View",
|
|
1552
|
+
file: "view"
|
|
1553
|
+
},
|
|
1554
|
+
{
|
|
1555
|
+
suffix: "Provider",
|
|
1556
|
+
file: "provider"
|
|
1557
|
+
}
|
|
1558
|
+
]) if (exportName.endsWith(s.suffix)) return `${exportName.slice(0, -s.suffix.length)}.${s.file}${ext}`;
|
|
1559
|
+
return `${exportName}${ext}`;
|
|
1560
|
+
}
|
|
1561
|
+
const rule$4 = {
|
|
1562
|
+
meta: {
|
|
1563
|
+
type: "problem",
|
|
1564
|
+
docs: { description: "Enforce that filenames match their single exported React component or single exported function." },
|
|
1565
|
+
schema: [{
|
|
1566
|
+
type: "object",
|
|
1567
|
+
properties: { ignoredFiles: {
|
|
1568
|
+
type: "array",
|
|
1569
|
+
items: { type: "string" }
|
|
1570
|
+
} },
|
|
1571
|
+
additionalProperties: false
|
|
1572
|
+
}],
|
|
1573
|
+
messages: { mismatch: "Filename \"{{filename}}\" should match the exported {{exportType}} name \"{{exportedName}}\". Expected \"{{expectedFilename}}\"." }
|
|
1574
|
+
},
|
|
1575
|
+
create(context) {
|
|
1576
|
+
const filename = context.filename;
|
|
1577
|
+
const base = node_path.default.basename(filename);
|
|
1578
|
+
const ext = node_path.default.extname(base);
|
|
1579
|
+
if ((getFirstOptionObject(context).ignoredFiles || [
|
|
1580
|
+
"utils.ts",
|
|
1581
|
+
"constants.ts",
|
|
1582
|
+
"types.ts",
|
|
1583
|
+
"index.ts",
|
|
1584
|
+
"index.tsx",
|
|
1585
|
+
"index.js",
|
|
1586
|
+
"index.jsx"
|
|
1587
|
+
]).includes(base)) return {};
|
|
1588
|
+
if (base.includes(".test.") || base.includes(".spec.") || base.includes(".stories.")) return {};
|
|
1589
|
+
const exported = [];
|
|
1590
|
+
const components = [];
|
|
1591
|
+
function recordExport(node, name, isComponent) {
|
|
1592
|
+
exported.push({
|
|
1593
|
+
name,
|
|
1594
|
+
node
|
|
1595
|
+
});
|
|
1596
|
+
if (isComponent) components.push({
|
|
1597
|
+
name,
|
|
1598
|
+
node
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
return {
|
|
1602
|
+
ExportNamedDeclaration(node) {
|
|
1603
|
+
const name = getExportedName(node);
|
|
1604
|
+
if (!name) return;
|
|
1605
|
+
recordExport(node, name, node.declaration ? isReactComponentExport(node.declaration) : isUppercaseComponentName(name));
|
|
1606
|
+
},
|
|
1607
|
+
ExportDefaultDeclaration(node) {
|
|
1608
|
+
const name = getExportedName(node);
|
|
1609
|
+
if (!name) return;
|
|
1610
|
+
let isComponent = false;
|
|
1611
|
+
const decl = node.declaration;
|
|
1612
|
+
if (decl && (decl.type === "FunctionDeclaration" || decl.type === "VariableDeclaration")) isComponent = isReactComponentExport(decl);
|
|
1613
|
+
else isComponent = isUppercaseComponentName(name);
|
|
1614
|
+
recordExport(node, name, isComponent);
|
|
1615
|
+
},
|
|
1616
|
+
"Program:exit"() {
|
|
1617
|
+
const filenameStem = stripExt(filename);
|
|
1618
|
+
if (components.length === 1) {
|
|
1619
|
+
const comp = components[0];
|
|
1620
|
+
const expectedFromFile = suffixExpectationFromFilename(base);
|
|
1621
|
+
let expectedExportName = filenameStem;
|
|
1622
|
+
if (expectedFromFile) expectedExportName = `${expectedFromFile.baseName}${expectedFromFile.suffix}`;
|
|
1623
|
+
else if ([
|
|
1624
|
+
"Page",
|
|
1625
|
+
"Module",
|
|
1626
|
+
"Layout",
|
|
1627
|
+
"View",
|
|
1628
|
+
"Provider"
|
|
1629
|
+
].some((s) => comp.name.endsWith(s))) {
|
|
1630
|
+
const expectedFilename = suggestFilenameFromExport(comp.name, ext);
|
|
1631
|
+
context.report({
|
|
1632
|
+
node: comp.node,
|
|
1633
|
+
messageId: "mismatch",
|
|
1634
|
+
data: {
|
|
1635
|
+
filename: base,
|
|
1636
|
+
exportType: "React component",
|
|
1637
|
+
exportedName: comp.name,
|
|
1638
|
+
expectedFilename
|
|
1639
|
+
}
|
|
1640
|
+
});
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
if (comp.name !== expectedExportName) {
|
|
1644
|
+
const expectedFilename = suggestFilenameFromExport(comp.name, ext);
|
|
1645
|
+
context.report({
|
|
1646
|
+
node: comp.node,
|
|
1647
|
+
messageId: "mismatch",
|
|
1648
|
+
data: {
|
|
1649
|
+
filename: base,
|
|
1650
|
+
exportType: "React component",
|
|
1651
|
+
exportedName: comp.name,
|
|
1652
|
+
expectedFilename
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
if (components.length === 0 && exported.length === 1) {
|
|
1659
|
+
const item = exported[0];
|
|
1660
|
+
if (item.name !== filenameStem) context.report({
|
|
1661
|
+
node: item.node,
|
|
1662
|
+
messageId: "mismatch",
|
|
1663
|
+
data: {
|
|
1664
|
+
filename: base,
|
|
1665
|
+
exportType: "function",
|
|
1666
|
+
exportedName: item.name,
|
|
1667
|
+
expectedFilename: `${item.name}${ext}`
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
var filename_match_export_default = rule$4;
|
|
1676
|
+
|
|
1677
|
+
//#endregion
|
|
1678
|
+
//#region src/rules/restrict-tsx-components.ts
|
|
1679
|
+
function isAllowedFile(filename) {
|
|
1680
|
+
return filename.endsWith(".page.tsx") || filename.endsWith(".page.ts") || filename.endsWith(".module.tsx") || filename.endsWith(".module.ts") || filename.endsWith(".layout.tsx") || filename.endsWith(".layout.ts") || filename.endsWith(".view.tsx") || filename.endsWith(".view.ts") || filename.endsWith(".provider.tsx") || filename.endsWith(".provider.ts");
|
|
1681
|
+
}
|
|
1682
|
+
function isComponentName(name) {
|
|
1683
|
+
return typeof name === "string" && name[0] === name[0].toUpperCase();
|
|
1684
|
+
}
|
|
1685
|
+
const rule$3 = {
|
|
1686
|
+
meta: {
|
|
1687
|
+
type: "problem",
|
|
1688
|
+
docs: { description: "Enforce that TSX components are only declared in *.page/module/layout/view/provider files (or allowlist)." },
|
|
1689
|
+
schema: [{
|
|
1690
|
+
type: "object",
|
|
1691
|
+
properties: { allowlist: {
|
|
1692
|
+
type: "array",
|
|
1693
|
+
items: { type: "string" }
|
|
1694
|
+
} },
|
|
1695
|
+
additionalProperties: false
|
|
1696
|
+
}],
|
|
1697
|
+
messages: { restricted: "Component \"{{componentName}}\" cannot be declared in this file. Use *.page/module/layout/view/provider.tsx or add it to the allowlist." }
|
|
1698
|
+
},
|
|
1699
|
+
create(context) {
|
|
1700
|
+
const filename = context.filename;
|
|
1701
|
+
if (isAllowedFile(filename)) return {};
|
|
1702
|
+
const base = node_path.default.basename(filename);
|
|
1703
|
+
if (base.includes(".test.") || base.includes(".spec.") || base.includes(".stories.")) return {};
|
|
1704
|
+
if (!filename.endsWith(".tsx")) return {};
|
|
1705
|
+
const opts = getFirstOptionObject(context);
|
|
1706
|
+
const allowlist = new Set(opts.allowlist ?? []);
|
|
1707
|
+
return {
|
|
1708
|
+
"ExportNamedDeclaration > FunctionDeclaration"(node) {
|
|
1709
|
+
const name = node.id?.name;
|
|
1710
|
+
if (!isComponentName(name)) return;
|
|
1711
|
+
if (allowlist.has(name)) return;
|
|
1712
|
+
context.report({
|
|
1713
|
+
node: node.id,
|
|
1714
|
+
messageId: "restricted",
|
|
1715
|
+
data: { componentName: name }
|
|
1716
|
+
});
|
|
1717
|
+
},
|
|
1718
|
+
"ExportNamedDeclaration > VariableDeclaration > VariableDeclarator"(node) {
|
|
1719
|
+
const name = node.id?.name;
|
|
1720
|
+
if (!isComponentName(name)) return;
|
|
1721
|
+
if (allowlist.has(name)) return;
|
|
1722
|
+
context.report({
|
|
1723
|
+
node: node.id,
|
|
1724
|
+
messageId: "restricted",
|
|
1725
|
+
data: { componentName: name }
|
|
1726
|
+
});
|
|
1727
|
+
},
|
|
1728
|
+
"ExportDefaultDeclaration > FunctionDeclaration"(node) {
|
|
1729
|
+
const name = node.id?.name;
|
|
1730
|
+
if (!isComponentName(name)) return;
|
|
1731
|
+
if (allowlist.has(name)) return;
|
|
1732
|
+
context.report({
|
|
1733
|
+
node: node.id,
|
|
1734
|
+
messageId: "restricted",
|
|
1735
|
+
data: { componentName: name }
|
|
1736
|
+
});
|
|
1737
|
+
},
|
|
1738
|
+
"ExportDefaultDeclaration > Identifier"(node) {
|
|
1739
|
+
const name = node.name;
|
|
1740
|
+
if (!isComponentName(name)) return;
|
|
1741
|
+
if (allowlist.has(name)) return;
|
|
1742
|
+
context.report({
|
|
1743
|
+
node,
|
|
1744
|
+
messageId: "restricted",
|
|
1745
|
+
data: { componentName: name }
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
var restrict_tsx_components_default = rule$3;
|
|
1752
|
+
|
|
1753
|
+
//#endregion
|
|
1754
|
+
//#region src/rules/no-dynamic-styled-components.ts
|
|
1755
|
+
function isInsideFunction(ancestors) {
|
|
1756
|
+
return ancestors.some((a) => [
|
|
1757
|
+
"FunctionDeclaration",
|
|
1758
|
+
"FunctionExpression",
|
|
1759
|
+
"ArrowFunctionExpression",
|
|
1760
|
+
"MethodDefinition",
|
|
1761
|
+
"Property",
|
|
1762
|
+
"PropertyDefinition"
|
|
1763
|
+
].includes(a.type));
|
|
1764
|
+
}
|
|
1765
|
+
function getLeftMostIdentifier(node) {
|
|
1766
|
+
let current = node;
|
|
1767
|
+
while (current) {
|
|
1768
|
+
if (current.type === "Identifier") return current.name;
|
|
1769
|
+
if (current.type === "MemberExpression") {
|
|
1770
|
+
current = current.object;
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
if (current.type === "CallExpression") {
|
|
1774
|
+
current = current.callee;
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
break;
|
|
1778
|
+
}
|
|
1779
|
+
return null;
|
|
1780
|
+
}
|
|
1781
|
+
const rule$2 = {
|
|
1782
|
+
meta: {
|
|
1783
|
+
type: "problem",
|
|
1784
|
+
docs: { description: "Disallow creating styled-components inside functions/components (must be module scoped)." },
|
|
1785
|
+
schema: [],
|
|
1786
|
+
messages: { dynamic: "Do not create styled-components inside functions/components. Hoist styled definitions to module scope." }
|
|
1787
|
+
},
|
|
1788
|
+
create(context) {
|
|
1789
|
+
const filename = context.filename;
|
|
1790
|
+
if (isTestSpecOrStoryFile(filename)) return {};
|
|
1791
|
+
const styledIdentifiers = /* @__PURE__ */ new Set();
|
|
1792
|
+
return {
|
|
1793
|
+
ImportDeclaration(node) {
|
|
1794
|
+
if (node.source?.value !== "styled-components") return;
|
|
1795
|
+
for (const spec of node.specifiers || []) if (spec.type === "ImportDefaultSpecifier" || spec.type === "ImportNamespaceSpecifier") styledIdentifiers.add(spec.local.name);
|
|
1796
|
+
},
|
|
1797
|
+
TaggedTemplateExpression(node) {
|
|
1798
|
+
if (styledIdentifiers.size === 0) return;
|
|
1799
|
+
const base = getLeftMostIdentifier(node.tag);
|
|
1800
|
+
if (!base || !styledIdentifiers.has(base)) return;
|
|
1801
|
+
if (isInsideFunction(context.sourceCode.getAncestors(node))) context.report({
|
|
1802
|
+
node,
|
|
1803
|
+
messageId: "dynamic"
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1809
|
+
var no_dynamic_styled_components_default = rule$2;
|
|
1810
|
+
|
|
1811
|
+
//#endregion
|
|
1812
|
+
//#region src/rules/no-plain-html-text-elements.ts
|
|
1813
|
+
const RESTRICTED = new Set([
|
|
1814
|
+
"div",
|
|
1815
|
+
"span",
|
|
1816
|
+
"p",
|
|
1817
|
+
"h1",
|
|
1818
|
+
"h2",
|
|
1819
|
+
"h3",
|
|
1820
|
+
"h4",
|
|
1821
|
+
"h5",
|
|
1822
|
+
"h6",
|
|
1823
|
+
"section",
|
|
1824
|
+
"article",
|
|
1825
|
+
"aside",
|
|
1826
|
+
"header",
|
|
1827
|
+
"footer",
|
|
1828
|
+
"main",
|
|
1829
|
+
"nav",
|
|
1830
|
+
"strong",
|
|
1831
|
+
"em",
|
|
1832
|
+
"b",
|
|
1833
|
+
"i",
|
|
1834
|
+
"u",
|
|
1835
|
+
"s",
|
|
1836
|
+
"small",
|
|
1837
|
+
"mark",
|
|
1838
|
+
"del",
|
|
1839
|
+
"ins",
|
|
1840
|
+
"sub",
|
|
1841
|
+
"sup",
|
|
1842
|
+
"blockquote",
|
|
1843
|
+
"cite",
|
|
1844
|
+
"q",
|
|
1845
|
+
"code",
|
|
1846
|
+
"pre",
|
|
1847
|
+
"kbd",
|
|
1848
|
+
"samp",
|
|
1849
|
+
"var",
|
|
1850
|
+
"time",
|
|
1851
|
+
"address",
|
|
1852
|
+
"abbr",
|
|
1853
|
+
"dfn",
|
|
1854
|
+
"label",
|
|
1855
|
+
"legend",
|
|
1856
|
+
"caption",
|
|
1857
|
+
"th",
|
|
1858
|
+
"td",
|
|
1859
|
+
"li",
|
|
1860
|
+
"dt",
|
|
1861
|
+
"dd",
|
|
1862
|
+
"figcaption",
|
|
1863
|
+
"summary"
|
|
1864
|
+
]);
|
|
1865
|
+
function hasDirectTextChildren(jsxElement) {
|
|
1866
|
+
const children = jsxElement?.children;
|
|
1867
|
+
if (!Array.isArray(children) || children.length === 0) return false;
|
|
1868
|
+
return children.some((child) => {
|
|
1869
|
+
if (!child) return false;
|
|
1870
|
+
if (child.type === "JSXText") return child.value.trim().length > 0;
|
|
1871
|
+
if (child.type === "JSXExpressionContainer" && child.expression) {
|
|
1872
|
+
const expr = child.expression;
|
|
1873
|
+
if (expr.type === "JSXEmptyExpression") return false;
|
|
1874
|
+
if (expr.type === "Literal") return typeof expr.value === "string" && expr.value.trim().length > 0;
|
|
1875
|
+
if (expr.type === "TemplateLiteral") return true;
|
|
1876
|
+
if (expr.type === "BinaryExpression" && expr.operator === "+") return true;
|
|
1877
|
+
if (expr.type === "ConditionalExpression") return true;
|
|
1878
|
+
if (expr.type === "Identifier") return !["null", "undefined"].includes(expr.name);
|
|
1879
|
+
if (expr.type === "MemberExpression") return true;
|
|
1880
|
+
}
|
|
1881
|
+
return false;
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
const rule$1 = {
|
|
1885
|
+
meta: {
|
|
1886
|
+
type: "problem",
|
|
1887
|
+
docs: { description: "Disallow wrapping text in plain HTML elements." },
|
|
1888
|
+
schema: [{
|
|
1889
|
+
type: "object",
|
|
1890
|
+
properties: {
|
|
1891
|
+
allowedElements: {
|
|
1892
|
+
type: "array",
|
|
1893
|
+
items: { type: "string" }
|
|
1894
|
+
},
|
|
1895
|
+
customMessage: { type: "string" }
|
|
1896
|
+
},
|
|
1897
|
+
additionalProperties: false
|
|
1898
|
+
}],
|
|
1899
|
+
messages: { plain: "Do not wrap text in plain HTML element <{{element}}>. Use a design-system typography component instead." }
|
|
1900
|
+
},
|
|
1901
|
+
create(context) {
|
|
1902
|
+
const opts = getFirstOptionObject(context);
|
|
1903
|
+
const allowed = new Set(opts.allowedElements ?? []);
|
|
1904
|
+
const customMessage = opts.customMessage;
|
|
1905
|
+
return { JSXOpeningElement(node) {
|
|
1906
|
+
if (node.name?.type !== "JSXIdentifier") return;
|
|
1907
|
+
const name = node.name.name;
|
|
1908
|
+
if (!name) return;
|
|
1909
|
+
if (name[0] === name[0].toUpperCase()) return;
|
|
1910
|
+
if (allowed.has(name)) return;
|
|
1911
|
+
if (!RESTRICTED.has(name)) return;
|
|
1912
|
+
const jsxElement = node.parent;
|
|
1913
|
+
if (jsxElement?.type !== "JSXElement") return;
|
|
1914
|
+
if (hasDirectTextChildren(jsxElement)) context.report({
|
|
1915
|
+
node,
|
|
1916
|
+
messageId: customMessage ? void 0 : "plain",
|
|
1917
|
+
message: customMessage,
|
|
1918
|
+
data: { element: name }
|
|
1919
|
+
});
|
|
1920
|
+
} };
|
|
1921
|
+
}
|
|
1922
|
+
};
|
|
1923
|
+
var no_plain_html_text_elements_default = rule$1;
|
|
1924
|
+
|
|
1925
|
+
//#endregion
|
|
1926
|
+
//#region src/rules/no-use-effect-in-hooks.ts
|
|
1927
|
+
function isInsideCustomHook(ancestors) {
|
|
1928
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
1929
|
+
const n = ancestors[i];
|
|
1930
|
+
if (n.type === "FunctionDeclaration" && n.id?.name?.startsWith("use")) return true;
|
|
1931
|
+
if (n.type === "VariableDeclarator" && n.id?.type === "Identifier" && n.id.name.startsWith("use")) return true;
|
|
1932
|
+
if (n.type === "ExportNamedDeclaration" && n.declaration?.type === "VariableDeclaration") {
|
|
1933
|
+
for (const d of n.declaration.declarations || []) if (d.id?.type === "Identifier" && d.id.name.startsWith("use")) return true;
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
return false;
|
|
1937
|
+
}
|
|
1938
|
+
const rule = {
|
|
1939
|
+
meta: {
|
|
1940
|
+
type: "problem",
|
|
1941
|
+
docs: { description: "Disallow effect hooks inside custom hooks." },
|
|
1942
|
+
schema: [],
|
|
1943
|
+
messages: { noEffect: "Do not use {{hookName}} inside custom hooks. Side effects should be managed by the component using the hook." }
|
|
1944
|
+
},
|
|
1945
|
+
create(context) {
|
|
1946
|
+
const filename = context.filename;
|
|
1947
|
+
if (isTestOrSpecFile(filename)) return {};
|
|
1948
|
+
const sourceCode = context.sourceCode;
|
|
1949
|
+
return { CallExpression(node) {
|
|
1950
|
+
const callee = node.callee;
|
|
1951
|
+
if (callee?.type !== "Identifier") return;
|
|
1952
|
+
const hookName = callee.name;
|
|
1953
|
+
if (!hookName || !hookName.toLowerCase().endsWith("effect")) return;
|
|
1954
|
+
if (isInsideCustomHook(sourceCode.getAncestors(node))) context.report({
|
|
1955
|
+
node,
|
|
1956
|
+
messageId: "noEffect",
|
|
1957
|
+
data: { hookName }
|
|
1958
|
+
});
|
|
1959
|
+
} };
|
|
1960
|
+
}
|
|
1961
|
+
};
|
|
1962
|
+
var no_use_effect_in_hooks_default = rule;
|
|
1963
|
+
|
|
1964
|
+
//#endregion
|
|
1965
|
+
//#region src/index.ts
|
|
1966
|
+
const plugin = {
|
|
1967
|
+
meta: {
|
|
1968
|
+
name: "@kingsword",
|
|
1969
|
+
version: "0.1.0"
|
|
1970
|
+
},
|
|
1971
|
+
rules: {
|
|
1972
|
+
"no-restricted-syntax": no_restricted_syntax_default,
|
|
1973
|
+
"no-exported-function-expressions": no_exported_function_expressions_default,
|
|
1974
|
+
"no-exported-string-union-types": no_exported_string_union_types_default,
|
|
1975
|
+
"structured-logging": structured_logging_default,
|
|
1976
|
+
"enum-file-organization": enum_file_organization_default,
|
|
1977
|
+
"types-file-organization": types_file_organization_default,
|
|
1978
|
+
"constants-file-organization": constants_file_organization_default,
|
|
1979
|
+
"errors-file-organization": errors_file_organization_default,
|
|
1980
|
+
"test-utils-organization": test_utils_organization_default,
|
|
1981
|
+
"test-file-location": test_file_location_default,
|
|
1982
|
+
"require-test-files": require_test_files_default,
|
|
1983
|
+
"require-tsx-test-stories-files": require_tsx_test_stories_files_default,
|
|
1984
|
+
"vitest-mock-absolute-paths": vitest_mock_absolute_paths_default,
|
|
1985
|
+
"vitest-mock-require-actual": vitest_mock_require_actual_default,
|
|
1986
|
+
"vitest-no-focused-tests": vitest_no_focused_tests_default,
|
|
1987
|
+
"vitest-no-disabled-tests": vitest_no_disabled_tests_default,
|
|
1988
|
+
"vitest-no-test-prefixes": vitest_no_test_prefixes_default,
|
|
1989
|
+
"no-log-exception-with-throw": no_log_exception_with_throw_default,
|
|
1990
|
+
"require-route-middleware": require_route_middleware_default,
|
|
1991
|
+
"require-v0-route-handle-middleware": require_v0_route_handle_middleware_default,
|
|
1992
|
+
"require-v0-strict-schemas": require_v0_strict_schemas_default,
|
|
1993
|
+
"filename-match-export": filename_match_export_default,
|
|
1994
|
+
"restrict-tsx-components": restrict_tsx_components_default,
|
|
1995
|
+
"no-dynamic-styled-components": no_dynamic_styled_components_default,
|
|
1996
|
+
"no-plain-html-text-elements": no_plain_html_text_elements_default,
|
|
1997
|
+
"no-use-effect-in-hooks": no_use_effect_in_hooks_default
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
var src_default = plugin;
|
|
2001
|
+
|
|
2002
|
+
//#endregion
|
|
2003
|
+
module.exports = src_default;
|