agent-eslint-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 +832 -0
- package/dist/index.d.mts +60 -0
- package/dist/index.mjs +760 -0
- package/package.json +61 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
import antfu from "@antfu/eslint-config";
|
|
2
|
+
import sonarjs from "eslint-plugin-sonarjs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AST_NODE_TYPES, ESLintUtils, TSESLint } from "@typescript-eslint/utils";
|
|
5
|
+
import validateFilename from "eslint-plugin-validate-filename";
|
|
6
|
+
import promisePlugin from "eslint-plugin-promise";
|
|
7
|
+
import tseslint from "typescript-eslint";
|
|
8
|
+
import * as ts from "typescript";
|
|
9
|
+
//#region src/configs/complexity.ts
|
|
10
|
+
/**
|
|
11
|
+
* Complexity-threshold rule group: ESLint-core cyclomatic/size limits plus
|
|
12
|
+
* SonarJS cognitive-complexity (its natural companion).
|
|
13
|
+
*
|
|
14
|
+
* Ownership (exclusive) — transcribed from `src/eslint.config.mjs` lines 40-46
|
|
15
|
+
* (core thresholds) and line 162 (`sonarjs/cognitive-complexity`). This builder,
|
|
16
|
+
* NOT `sonarjs.ts`, owns cognitive-complexity so both complexity dimensions
|
|
17
|
+
* (cyclomatic + cognitive) live together.
|
|
18
|
+
*
|
|
19
|
+
* antfu does NOT bundle `eslint-plugin-sonarjs`, so the plugin is registered here
|
|
20
|
+
* (safe to re-register the same reference across sibling builders).
|
|
21
|
+
*
|
|
22
|
+
* @param _options - Resolved factory options (unused: this group is static).
|
|
23
|
+
* @returns The complexity-threshold flat-config items.
|
|
24
|
+
*/
|
|
25
|
+
function complexityConfig(_options) {
|
|
26
|
+
return [{
|
|
27
|
+
plugins: { sonarjs },
|
|
28
|
+
rules: {
|
|
29
|
+
"complexity": ["error", 10],
|
|
30
|
+
"max-depth": ["error", 2],
|
|
31
|
+
"max-lines-per-function": ["error", {
|
|
32
|
+
max: 40,
|
|
33
|
+
skipBlankLines: true,
|
|
34
|
+
skipComments: true
|
|
35
|
+
}],
|
|
36
|
+
"max-statements": ["error", 10],
|
|
37
|
+
"max-lines": ["error", {
|
|
38
|
+
max: 150,
|
|
39
|
+
skipBlankLines: true,
|
|
40
|
+
skipComments: true
|
|
41
|
+
}],
|
|
42
|
+
"max-nested-callbacks": ["error", 3],
|
|
43
|
+
"max-params": ["error", 3],
|
|
44
|
+
"sonarjs/cognitive-complexity": ["error", 4]
|
|
45
|
+
}
|
|
46
|
+
}];
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/types.ts
|
|
50
|
+
/**
|
|
51
|
+
* Default alias settings applied when a consumer passes no `alias` option (or
|
|
52
|
+
* omits one of its fields): the `@` prefix mapped to the `src` directory.
|
|
53
|
+
*/
|
|
54
|
+
const DEFAULT_ALIAS = {
|
|
55
|
+
prefix: "@",
|
|
56
|
+
sourceDir: "src"
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/plugins/root-alias.ts
|
|
60
|
+
const createRule$1 = ESLintUtils.RuleCreator((name) => `https://github.com/NeoLabHQ/agent-eslint-config/blob/main/docs/rules/${name}.md`);
|
|
61
|
+
/**
|
|
62
|
+
* Returns true for a same-directory import of at most two path parts (`./foo`),
|
|
63
|
+
* which the original rule intentionally allows to stay relative.
|
|
64
|
+
*
|
|
65
|
+
* @param importPath - the raw import specifier (e.g. `./foo`, `../a/b`)
|
|
66
|
+
*/
|
|
67
|
+
function isAllowedSameDirImport(importPath) {
|
|
68
|
+
const pathParts = importPath.split("/");
|
|
69
|
+
return pathParts[0] === "." && pathParts.length <= 2;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Resolves an import specifier to its path relative to the project root, split
|
|
73
|
+
* into segments. Returns `null` when the specifier is not a relative import.
|
|
74
|
+
*
|
|
75
|
+
* @param importPath - the raw import specifier
|
|
76
|
+
* @param filename - the absolute-or-relative path of the importing file
|
|
77
|
+
* @param cwd - the project root the resulting path is made relative to
|
|
78
|
+
*/
|
|
79
|
+
function resolveImportSegments(importPath, filename, cwd) {
|
|
80
|
+
const currentFileDirectory = path.dirname(filename);
|
|
81
|
+
const importFilePath = path.resolve(currentFileDirectory, importPath);
|
|
82
|
+
return path.relative(cwd, importFilePath).split(path.sep);
|
|
83
|
+
}
|
|
84
|
+
var root_alias_default = {
|
|
85
|
+
meta: {
|
|
86
|
+
name: "eslint-plugin-root-alias",
|
|
87
|
+
version: "1.0.0"
|
|
88
|
+
},
|
|
89
|
+
rules: { "prefer-alias": createRule$1({
|
|
90
|
+
name: "prefer-alias",
|
|
91
|
+
meta: {
|
|
92
|
+
type: "suggestion",
|
|
93
|
+
fixable: "code",
|
|
94
|
+
docs: { description: "Prefer the source-root alias over relative imports that reach into the source directory." },
|
|
95
|
+
messages: { default: "Use alias \"{{alias}}\" instead of relative import \"{{relative}}\"" },
|
|
96
|
+
schema: [{
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
prefix: { type: "string" },
|
|
100
|
+
sourceDir: { type: "string" }
|
|
101
|
+
},
|
|
102
|
+
additionalProperties: false
|
|
103
|
+
}]
|
|
104
|
+
},
|
|
105
|
+
defaultOptions: [DEFAULT_ALIAS],
|
|
106
|
+
create(context, [options]) {
|
|
107
|
+
const prefix = options.prefix ?? DEFAULT_ALIAS.prefix;
|
|
108
|
+
const sourceDir = options.sourceDir ?? DEFAULT_ALIAS.sourceDir;
|
|
109
|
+
return { ImportDeclaration(node) {
|
|
110
|
+
const importPath = node.source.value;
|
|
111
|
+
if (importPath.indexOf(".") !== 0) return;
|
|
112
|
+
if (isAllowedSameDirImport(importPath)) return;
|
|
113
|
+
const importFilePathParts = resolveImportSegments(importPath, context.filename, context.cwd);
|
|
114
|
+
if (importFilePathParts.shift() !== sourceDir) return;
|
|
115
|
+
const aliasedImport = [prefix, ...importFilePathParts].join("/");
|
|
116
|
+
context.report({
|
|
117
|
+
node: node.source,
|
|
118
|
+
messageId: "default",
|
|
119
|
+
data: {
|
|
120
|
+
alias: aliasedImport,
|
|
121
|
+
relative: importPath
|
|
122
|
+
},
|
|
123
|
+
fix: (fixer) => fixer.replaceText(node.source, `'${aliasedImport}'`)
|
|
124
|
+
});
|
|
125
|
+
} };
|
|
126
|
+
}
|
|
127
|
+
}) }
|
|
128
|
+
};
|
|
129
|
+
const plugin = {
|
|
130
|
+
meta: {
|
|
131
|
+
name: "eslint-plugin-step-down-rule",
|
|
132
|
+
version: "1.0.0"
|
|
133
|
+
},
|
|
134
|
+
rules: { "step-down": ESLintUtils.RuleCreator((name) => `https://github.com/NeoLabHQ/agent-eslint-config/blob/main/docs/rules/${name}.md`)({
|
|
135
|
+
name: "step-down",
|
|
136
|
+
meta: {
|
|
137
|
+
type: "suggestion",
|
|
138
|
+
docs: { description: "Enforce top-down call structure — callers appear before callees." },
|
|
139
|
+
messages: { stepDown: "Expected '{{callee}}' to be defined after '{{caller}}'. Follow top-to-bottom definitions" },
|
|
140
|
+
schema: []
|
|
141
|
+
},
|
|
142
|
+
defaultOptions: [],
|
|
143
|
+
create(context) {
|
|
144
|
+
const sourceCode = context.sourceCode;
|
|
145
|
+
let requiredFunctions = [];
|
|
146
|
+
let methodStack = [];
|
|
147
|
+
return {
|
|
148
|
+
Program(node) {
|
|
149
|
+
methodStack = [];
|
|
150
|
+
requiredFunctions = [];
|
|
151
|
+
findVariablesInScope(sourceCode.getScope(node));
|
|
152
|
+
},
|
|
153
|
+
MethodDefinition(node) {
|
|
154
|
+
if (node.kind === "method") methodStack.push("name" in node.key ? node.key.name : void 0);
|
|
155
|
+
},
|
|
156
|
+
"CallExpression:exit"(node) {
|
|
157
|
+
if (node.callee.type !== AST_NODE_TYPES.MemberExpression || node.callee.property.type !== AST_NODE_TYPES.Identifier) return;
|
|
158
|
+
if (node.callee.object.type !== AST_NODE_TYPES.ThisExpression) return;
|
|
159
|
+
const calledMethodName = node.callee.property.name;
|
|
160
|
+
const calledIndex = methodStack.indexOf(calledMethodName);
|
|
161
|
+
if (calledIndex === -1 || calledIndex >= methodStack.length - 1) return;
|
|
162
|
+
const callerName = methodStack[methodStack.length - 1] ?? "(unknown)";
|
|
163
|
+
context.report({
|
|
164
|
+
node,
|
|
165
|
+
messageId: "stepDown",
|
|
166
|
+
data: {
|
|
167
|
+
callee: calledMethodName,
|
|
168
|
+
caller: callerName
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
function findVariablesInScope(scope) {
|
|
174
|
+
for (const reference of scope.references) {
|
|
175
|
+
const variable = reference.resolved;
|
|
176
|
+
collectRequiredFunction(reference);
|
|
177
|
+
if (!isViolation(reference, variable)) continue;
|
|
178
|
+
context.report({
|
|
179
|
+
node: reference.identifier,
|
|
180
|
+
messageId: "stepDown",
|
|
181
|
+
data: {
|
|
182
|
+
callee: reference.identifier.name,
|
|
183
|
+
caller: resolveCallerName(reference)
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
for (const childScope of scope.childScopes) findVariablesInScope(childScope);
|
|
188
|
+
}
|
|
189
|
+
function collectRequiredFunction(reference) {
|
|
190
|
+
const grandParent = reference.identifier.parent?.parent;
|
|
191
|
+
if (reference.identifier.name === "require" && grandParent?.type === AST_NODE_TYPES.VariableDeclarator && grandParent.id.type === AST_NODE_TYPES.Identifier) requiredFunctions.push(grandParent.id.name);
|
|
192
|
+
}
|
|
193
|
+
function isViolation(reference, variable) {
|
|
194
|
+
if (reference.init || !variable || variable.identifiers.length === 0) return false;
|
|
195
|
+
if (isInsideDecorator(reference)) return false;
|
|
196
|
+
if (isNotAFunctionCall(reference)) return false;
|
|
197
|
+
if (isCallingDown(variable, reference) && !isSameScope(variable, reference)) return false;
|
|
198
|
+
if (isSameScope(variable, reference) && !isCallingDown(variable, reference)) return false;
|
|
199
|
+
if (requiredFunctions.includes(reference.identifier.name)) return false;
|
|
200
|
+
if (isNotADeclaration(variable)) return false;
|
|
201
|
+
if (isRecursiveFunction(reference.identifier, reference.identifier.name)) return false;
|
|
202
|
+
if (!isOuterVariable(variable, reference) && !isFunctionDef(variable)) return false;
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
function isInsideDecorator(reference) {
|
|
206
|
+
let node = reference.identifier.parent;
|
|
207
|
+
while (node) {
|
|
208
|
+
if (node.type === AST_NODE_TYPES.Decorator) return true;
|
|
209
|
+
node = node.parent;
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
function isNotAFunctionCall(reference) {
|
|
214
|
+
const parent = reference.identifier.parent;
|
|
215
|
+
return parent.type !== AST_NODE_TYPES.CallExpression || parent.callee.type !== AST_NODE_TYPES.Identifier || reference.identifier.name !== parent.callee.name;
|
|
216
|
+
}
|
|
217
|
+
function isCallingDown(variable, reference) {
|
|
218
|
+
return variable.identifiers[0].range[1] > reference.identifier.range[1];
|
|
219
|
+
}
|
|
220
|
+
function isSameScope(variable, reference) {
|
|
221
|
+
return variable.scope.variableScope === reference.from.variableScope;
|
|
222
|
+
}
|
|
223
|
+
function isNotADeclaration(variable) {
|
|
224
|
+
const { type } = variable.identifiers[0].parent;
|
|
225
|
+
return type !== AST_NODE_TYPES.VariableDeclarator && type !== AST_NODE_TYPES.FunctionDeclaration;
|
|
226
|
+
}
|
|
227
|
+
function isRecursiveFunction(identifier, funcName) {
|
|
228
|
+
const parent = identifier.parent;
|
|
229
|
+
if (!parent) return false;
|
|
230
|
+
if ("id" in parent && parent.id && "name" in parent.id && parent.id.name === funcName) return true;
|
|
231
|
+
return isRecursiveFunction(parent, funcName);
|
|
232
|
+
}
|
|
233
|
+
function isOuterVariable(variable, reference) {
|
|
234
|
+
return variable.defs[0].type === TSESLint.Scope.DefinitionType.Variable && variable.scope.variableScope !== reference.from.variableScope;
|
|
235
|
+
}
|
|
236
|
+
function isFunctionDef(variable) {
|
|
237
|
+
return variable.defs[0].type === TSESLint.Scope.DefinitionType.FunctionName;
|
|
238
|
+
}
|
|
239
|
+
function resolveCallerName(reference) {
|
|
240
|
+
let scope = reference.from;
|
|
241
|
+
while (scope) {
|
|
242
|
+
const name = nameFromScope(scope);
|
|
243
|
+
if (name) return name;
|
|
244
|
+
scope = scope.upper;
|
|
245
|
+
}
|
|
246
|
+
return "(unknown)";
|
|
247
|
+
}
|
|
248
|
+
function nameFromScope(scope) {
|
|
249
|
+
const block = scope.block;
|
|
250
|
+
if ("id" in block && block.id && "name" in block.id) return block.id.name;
|
|
251
|
+
const parent = block.parent;
|
|
252
|
+
if (parent?.type === AST_NODE_TYPES.VariableDeclarator && parent.id.type === AST_NODE_TYPES.Identifier) return parent.id.name;
|
|
253
|
+
if (parent?.type === AST_NODE_TYPES.Property && parent.key.type === AST_NODE_TYPES.Identifier) return parent.key.name;
|
|
254
|
+
if (parent?.type === AST_NODE_TYPES.MethodDefinition && parent.key.type === AST_NODE_TYPES.Identifier) return parent.key.name;
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}) }
|
|
259
|
+
};
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/configs/custom.ts
|
|
262
|
+
/**
|
|
263
|
+
* Custom-plugin rule group: wires the two AST-only custom rules — `step-down`
|
|
264
|
+
* (always on) and `prefer-alias` (gated on the `alias` option).
|
|
265
|
+
*
|
|
266
|
+
* Ownership (exclusive) — transcribed from `src/eslint.config.mjs` lines 12-19
|
|
267
|
+
* (`alias` plugin + `prefer-alias` rule) and line 37 (`step-down`). The
|
|
268
|
+
* type-aware `no-never-return` rule is owned by `type-aware.ts`, not here.
|
|
269
|
+
*
|
|
270
|
+
* antfu bundles neither plugin, so both are registered by this builder.
|
|
271
|
+
*
|
|
272
|
+
* The `alias` gating is the only place the bespoke `alias` option changes the
|
|
273
|
+
* emitted rule set: when disabled (`alias: false`) the `prefer-alias` rule and
|
|
274
|
+
* its plugin are omitted entirely; otherwise the resolved `{ prefix, sourceDir }`
|
|
275
|
+
* are fed straight into the rule's options so its reports and autofix use them.
|
|
276
|
+
*
|
|
277
|
+
* @param options - Resolved factory options; `options.alias` gates `prefer-alias`.
|
|
278
|
+
* @returns The custom-plugin flat-config items.
|
|
279
|
+
*/
|
|
280
|
+
function customConfig(options) {
|
|
281
|
+
const items = [{
|
|
282
|
+
plugins: { "step-down-rule": plugin },
|
|
283
|
+
rules: { "step-down-rule/step-down": "error" }
|
|
284
|
+
}];
|
|
285
|
+
if (options.alias !== false) items.push({
|
|
286
|
+
plugins: { alias: root_alias_default },
|
|
287
|
+
rules: { "alias/prefer-alias": ["error", {
|
|
288
|
+
prefix: options.alias.prefix,
|
|
289
|
+
sourceDir: options.alias.sourceDir
|
|
290
|
+
}] }
|
|
291
|
+
});
|
|
292
|
+
return items;
|
|
293
|
+
}
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region src/configs/jsdoc.ts
|
|
296
|
+
/**
|
|
297
|
+
* JSDoc documentation-enforcement rule group.
|
|
298
|
+
*
|
|
299
|
+
* Ownership (exclusive) — transcribed from `src/eslint.config.mjs` lines 71-90.
|
|
300
|
+
*
|
|
301
|
+
* antfu already bundles and registers `eslint-plugin-jsdoc`, so this builder
|
|
302
|
+
* only overrides rule severities; it does NOT re-register the plugin.
|
|
303
|
+
*
|
|
304
|
+
* @param _options - Resolved factory options (unused: this group is static).
|
|
305
|
+
* @returns The JSDoc flat-config items.
|
|
306
|
+
*/
|
|
307
|
+
function jsdocConfig(_options) {
|
|
308
|
+
return [{ rules: {
|
|
309
|
+
"jsdoc/require-jsdoc": ["error", {
|
|
310
|
+
require: {
|
|
311
|
+
FunctionDeclaration: true,
|
|
312
|
+
MethodDefinition: true,
|
|
313
|
+
ClassDeclaration: true,
|
|
314
|
+
ArrowFunctionExpression: false,
|
|
315
|
+
FunctionExpression: false
|
|
316
|
+
},
|
|
317
|
+
checkConstructors: true,
|
|
318
|
+
checkGetters: true,
|
|
319
|
+
checkSetters: true
|
|
320
|
+
}],
|
|
321
|
+
"jsdoc/require-description": "error",
|
|
322
|
+
"jsdoc/require-param": "error",
|
|
323
|
+
"jsdoc/require-returns": "error",
|
|
324
|
+
"jsdoc/check-param-names": "error",
|
|
325
|
+
"jsdoc/no-blank-blocks": "error"
|
|
326
|
+
} }];
|
|
327
|
+
}
|
|
328
|
+
//#endregion
|
|
329
|
+
//#region src/configs/naming.ts
|
|
330
|
+
/**
|
|
331
|
+
* Naming-convention rule group: bans vague identifier/file names
|
|
332
|
+
* (`util`/`common`/`helper`/`function`) and disables antfu's lowercase-title rule.
|
|
333
|
+
*
|
|
334
|
+
* Ownership (exclusive) — transcribed from `src/eslint.config.mjs` lines 22-33
|
|
335
|
+
* (validate-filename + test override) and lines 130-155 (sonar naming rules).
|
|
336
|
+
* The three sonar naming rules live here, NOT in `sonarjs.ts`.
|
|
337
|
+
*
|
|
338
|
+
* antfu bundles neither `eslint-plugin-sonarjs` nor `eslint-plugin-validate-filename`,
|
|
339
|
+
* so both are registered here; `test/prefer-lowercase-title` overrides antfu's
|
|
340
|
+
* already-registered `test` plugin default.
|
|
341
|
+
*
|
|
342
|
+
* @param _options - Resolved factory options (unused: this group is static).
|
|
343
|
+
* @returns The naming-convention flat-config items.
|
|
344
|
+
*/
|
|
345
|
+
function namingConfig(_options) {
|
|
346
|
+
return [{
|
|
347
|
+
plugins: {
|
|
348
|
+
"sonarjs": sonarjs,
|
|
349
|
+
"validate-filename": validateFilename
|
|
350
|
+
},
|
|
351
|
+
rules: {
|
|
352
|
+
"validate-filename/naming-rules": ["error", { rules: [{
|
|
353
|
+
target: "**/*.ts",
|
|
354
|
+
patterns: "^(?!.*(util|common|helper|function)).+$"
|
|
355
|
+
}] }],
|
|
356
|
+
"test/prefer-lowercase-title": "off",
|
|
357
|
+
"sonarjs/class-name": ["error", { format: "^(?!.*(util|Util|UTIL|common|Common|COMMON|helper|Helper|HELPER|function|Function|FUNCTION))[A-Z][a-zA-Z0-9]*$" }],
|
|
358
|
+
"sonarjs/function-name": ["error", { format: "^(?!.*(util|Util|UTIL|common|Common|COMMON|helper|Helper|HELPER|function|Function|FUNCTION))[a-zA-Z][a-zA-Z0-9]*$" }],
|
|
359
|
+
"sonarjs/variable-name": ["error", { format: "^(?!.*(util|Util|UTIL|common|Common|COMMON|helper|Helper|HELPER|function|Function|FUNCTION))([a-z][a-zA-Z0-9]*|[A-Z][A-Z0-9_]*|[A-Z][a-zA-Z0-9]*|_{1,5})$" }]
|
|
360
|
+
}
|
|
361
|
+
}];
|
|
362
|
+
}
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region src/configs/promise.ts
|
|
365
|
+
/**
|
|
366
|
+
* Promise rule group: enforces `await` over `.then()` chaining.
|
|
367
|
+
*
|
|
368
|
+
* Ownership (exclusive) — transcribed from `src/eslint.config.mjs` line 5 (plugin
|
|
369
|
+
* import/registration) and line 35 (`promise/prefer-await-to-then`).
|
|
370
|
+
*
|
|
371
|
+
* antfu does NOT bundle `eslint-plugin-promise`, so this builder registers the
|
|
372
|
+
* plugin itself; omitting it would silently drop the rule.
|
|
373
|
+
*
|
|
374
|
+
* @param _options - Resolved factory options (unused: this group is static).
|
|
375
|
+
* @returns The promise flat-config items.
|
|
376
|
+
*/
|
|
377
|
+
function promiseConfig(_options) {
|
|
378
|
+
return [{
|
|
379
|
+
plugins: { promise: promisePlugin },
|
|
380
|
+
rules: { "promise/prefer-await-to-then": "error" }
|
|
381
|
+
}];
|
|
382
|
+
}
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region src/configs/sonarjs.ts
|
|
385
|
+
/**
|
|
386
|
+
* SonarJS rule group: control-flow, dead-code, nesting, loops, functions,
|
|
387
|
+
* promises, security, and testing rules.
|
|
388
|
+
*
|
|
389
|
+
* Ownership (exclusive) — transcribed from `src/eslint.config.mjs` lines 163-225.
|
|
390
|
+
* Deliberately EXCLUDES the two sonar rule sets owned by sibling builders:
|
|
391
|
+
* - the 3 naming rules (`class-name`/`function-name`/`variable-name`, lines
|
|
392
|
+
* 130-155) → owned by `naming.ts`
|
|
393
|
+
* - `sonarjs/cognitive-complexity` (line 162) → owned by `complexity.ts`
|
|
394
|
+
*
|
|
395
|
+
* antfu does NOT bundle `eslint-plugin-sonarjs`, so this builder registers the
|
|
396
|
+
* plugin. Registering it here (and again in `naming.ts`/`complexity.ts`) is safe:
|
|
397
|
+
* flat config shallow-merges identical plugin references without conflict.
|
|
398
|
+
*
|
|
399
|
+
* @param _options - Resolved factory options (unused: this group is static).
|
|
400
|
+
* @returns The SonarJS flat-config items.
|
|
401
|
+
*/
|
|
402
|
+
function sonarjsConfig(_options) {
|
|
403
|
+
return [{
|
|
404
|
+
plugins: { sonarjs },
|
|
405
|
+
rules: {
|
|
406
|
+
"sonarjs/nested-control-flow": ["error", { maximumNestingLevel: 2 }],
|
|
407
|
+
"sonarjs/too-many-break-or-continue-in-loop": "error",
|
|
408
|
+
"sonarjs/elseif-without-else": "error",
|
|
409
|
+
"sonarjs/no-nested-conditional": "error",
|
|
410
|
+
"sonarjs/no-same-line-conditional": "error",
|
|
411
|
+
"sonarjs/conditional-indentation": "error",
|
|
412
|
+
"sonarjs/no-all-duplicated-branches": "error",
|
|
413
|
+
"sonarjs/no-duplicated-branches": "error",
|
|
414
|
+
"sonarjs/no-dead-store": "error",
|
|
415
|
+
"sonarjs/no-redundant-assignments": "error",
|
|
416
|
+
"sonarjs/no-identical-functions": ["error", 3],
|
|
417
|
+
"sonarjs/no-useless-catch": "error",
|
|
418
|
+
"sonarjs/no-useless-increment": "error",
|
|
419
|
+
"sonarjs/useless-string-operation": "error",
|
|
420
|
+
"sonarjs/prefer-immediate-return": "error",
|
|
421
|
+
"sonarjs/no-nested-assignment": "error",
|
|
422
|
+
"sonarjs/no-nested-functions": "error",
|
|
423
|
+
"sonarjs/no-nested-incdec": "error",
|
|
424
|
+
"sonarjs/no-parameter-reassignment": "error",
|
|
425
|
+
"sonarjs/destructuring-assignment-syntax": "error",
|
|
426
|
+
"sonarjs/misplaced-loop-counter": "error",
|
|
427
|
+
"sonarjs/updated-loop-counter": "error",
|
|
428
|
+
"sonarjs/no-function-declaration-in-block": "error",
|
|
429
|
+
"sonarjs/no-globals-shadowing": "error",
|
|
430
|
+
"sonarjs/no-fallthrough": "error",
|
|
431
|
+
"sonarjs/no-reference-error": "error",
|
|
432
|
+
"sonarjs/no-unthrown-error": "error",
|
|
433
|
+
"sonarjs/prefer-type-guard": "error",
|
|
434
|
+
"sonarjs/no-try-promise": "error",
|
|
435
|
+
"sonarjs/no-hardcoded-ip": "error",
|
|
436
|
+
"sonarjs/no-hardcoded-passwords": "error",
|
|
437
|
+
"sonarjs/no-hardcoded-secrets": "error",
|
|
438
|
+
"sonarjs/os-command": "error",
|
|
439
|
+
"sonarjs/no-skipped-tests": "error",
|
|
440
|
+
"sonarjs/stable-tests": "error"
|
|
441
|
+
}
|
|
442
|
+
}];
|
|
443
|
+
}
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region src/configs/stylistic.ts
|
|
446
|
+
/**
|
|
447
|
+
* Stylistic / code-shape rule group: ESLint-core formatting and declaration
|
|
448
|
+
* rules, static-member bans, and select `@typescript-eslint`/`perfectionist`
|
|
449
|
+
* overrides.
|
|
450
|
+
*
|
|
451
|
+
* Ownership (exclusive) — transcribed from `src/eslint.config.mjs` lines 34, 36,
|
|
452
|
+
* 114, 119-123, and 230-281.
|
|
453
|
+
*
|
|
454
|
+
* All referenced plugins (`ts`/`@typescript-eslint`, `perfectionist`) are already
|
|
455
|
+
* registered by antfu, so this builder only overrides rule severities/options.
|
|
456
|
+
*
|
|
457
|
+
* @param _options - Resolved factory options (unused: this group is static).
|
|
458
|
+
* @returns The stylistic flat-config items.
|
|
459
|
+
*/
|
|
460
|
+
function stylisticConfig(_options) {
|
|
461
|
+
return [{ rules: {
|
|
462
|
+
"ts/consistent-type-imports": "off",
|
|
463
|
+
"perfectionist/sort-named-imports": "off",
|
|
464
|
+
"@typescript-eslint/consistent-type-definitions": ["error", "interface"],
|
|
465
|
+
"class-methods-use-this": "off",
|
|
466
|
+
"@typescript-eslint/class-methods-use-this": ["error", {
|
|
467
|
+
ignoreOverrideMethods: true,
|
|
468
|
+
ignoreClassesThatImplementAnInterface: "public-fields"
|
|
469
|
+
}],
|
|
470
|
+
"no-warning-comments": ["error", {
|
|
471
|
+
terms: ["jscpd:ignore-start", "jscpd:ignore-end"],
|
|
472
|
+
location: "anywhere"
|
|
473
|
+
}],
|
|
474
|
+
"prefer-const": "error",
|
|
475
|
+
"init-declarations": ["error", "always"],
|
|
476
|
+
"id-length": ["error", {
|
|
477
|
+
min: 3,
|
|
478
|
+
max: 35,
|
|
479
|
+
exceptions: [
|
|
480
|
+
"i",
|
|
481
|
+
"j",
|
|
482
|
+
"k",
|
|
483
|
+
"x",
|
|
484
|
+
"y",
|
|
485
|
+
"z",
|
|
486
|
+
"_",
|
|
487
|
+
"id",
|
|
488
|
+
"on",
|
|
489
|
+
"in",
|
|
490
|
+
"of"
|
|
491
|
+
]
|
|
492
|
+
}],
|
|
493
|
+
"padding-line-between-statements": [
|
|
494
|
+
"error",
|
|
495
|
+
{
|
|
496
|
+
blankLine: "always",
|
|
497
|
+
prev: ["const", "let"],
|
|
498
|
+
next: "*"
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
blankLine: "any",
|
|
502
|
+
prev: ["const", "let"],
|
|
503
|
+
next: ["const", "let"]
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
blankLine: "always",
|
|
507
|
+
prev: "*",
|
|
508
|
+
next: "return"
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
blankLine: "always",
|
|
512
|
+
prev: "*",
|
|
513
|
+
next: [
|
|
514
|
+
"if",
|
|
515
|
+
"for",
|
|
516
|
+
"while",
|
|
517
|
+
"switch",
|
|
518
|
+
"try"
|
|
519
|
+
]
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
blankLine: "any",
|
|
523
|
+
prev: ["const", "let"],
|
|
524
|
+
next: "if"
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
blankLine: "always",
|
|
528
|
+
prev: [
|
|
529
|
+
"if",
|
|
530
|
+
"for",
|
|
531
|
+
"while",
|
|
532
|
+
"switch",
|
|
533
|
+
"try"
|
|
534
|
+
],
|
|
535
|
+
next: "*"
|
|
536
|
+
}
|
|
537
|
+
],
|
|
538
|
+
"preserve-caught-error": "error",
|
|
539
|
+
"no-restricted-syntax": [
|
|
540
|
+
"error",
|
|
541
|
+
{
|
|
542
|
+
selector: "MethodDefinition[static=true]",
|
|
543
|
+
message: "Do not use static methods. Convert to an instance method or extract to a standalone function."
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
selector: "PropertyDefinition[static=true]",
|
|
547
|
+
message: "Do not use static properties. Use instance properties instead."
|
|
548
|
+
}
|
|
549
|
+
]
|
|
550
|
+
} }];
|
|
551
|
+
}
|
|
552
|
+
var no_never_return_type_default = {
|
|
553
|
+
meta: {
|
|
554
|
+
name: "eslint-plugin-no-never-return-type",
|
|
555
|
+
version: "1.0.0"
|
|
556
|
+
},
|
|
557
|
+
rules: { "no-never-return-type": ESLintUtils.RuleCreator((name) => `https://github.com/NeoLabHQ/agent-eslint-config/blob/main/docs/eslint-rules/${name}`)({
|
|
558
|
+
name: "no-never-return-type",
|
|
559
|
+
meta: {
|
|
560
|
+
type: "problem",
|
|
561
|
+
docs: { description: "Disallow functions that return `never`. Restructure to throw at the call site." },
|
|
562
|
+
messages: { noNeverReturn: "Function has return type `never`. Throw directly at the call site instead of wrapping error in a function or throwing as side effect during validation." },
|
|
563
|
+
schema: []
|
|
564
|
+
},
|
|
565
|
+
defaultOptions: [],
|
|
566
|
+
create(context) {
|
|
567
|
+
const services = ESLintUtils.getParserServices(context);
|
|
568
|
+
const checker = services.program.getTypeChecker();
|
|
569
|
+
/**
|
|
570
|
+
* Returns true if the given function-like node resolves to a `never`
|
|
571
|
+
* return type in the TypeScript type checker.
|
|
572
|
+
*
|
|
573
|
+
* @param functionNode - ESTree function-like node (declaration, expression, or arrow)
|
|
574
|
+
* @returns whether the function's return type is `never`
|
|
575
|
+
*/
|
|
576
|
+
function hasNeverReturnType(functionNode) {
|
|
577
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(functionNode);
|
|
578
|
+
const signature = checker.getSignatureFromDeclaration(tsNode);
|
|
579
|
+
if (!signature) return false;
|
|
580
|
+
const returnType = checker.getReturnTypeOfSignature(signature);
|
|
581
|
+
return Boolean(returnType.getFlags() & ts.TypeFlags.Never);
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Returns true when the function node is used as a callback — either as
|
|
585
|
+
* an object property value or as an argument in a function/method call.
|
|
586
|
+
* These are intentional throw-only callbacks (e.g. Catch decorator handlers)
|
|
587
|
+
* and should not be flagged.
|
|
588
|
+
*
|
|
589
|
+
* @param node - ESTree function-like node to inspect
|
|
590
|
+
* @returns whether the function is a callback
|
|
591
|
+
*/
|
|
592
|
+
function isCallbackFunction(node) {
|
|
593
|
+
const { parent } = node;
|
|
594
|
+
if (parent.type === "Property" && parent.value === node) return true;
|
|
595
|
+
if (parent.type === "CallExpression" && parent.arguments.includes(node)) return true;
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Reports `reportNode` when `functionNode` resolves to a `never` return type.
|
|
600
|
+
*
|
|
601
|
+
* `reportNode` and `functionNode` differ only for class methods, where the
|
|
602
|
+
* inner function is type-checked but the `MethodDefinition` is reported on.
|
|
603
|
+
*
|
|
604
|
+
* @param reportNode - node the violation is reported against
|
|
605
|
+
* @param functionNode - function-like node whose return type is inspected
|
|
606
|
+
*/
|
|
607
|
+
function checkAndReport(reportNode, functionNode) {
|
|
608
|
+
if (hasNeverReturnType(functionNode)) context.report({
|
|
609
|
+
node: reportNode,
|
|
610
|
+
messageId: "noNeverReturn"
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
FunctionDeclaration(node) {
|
|
615
|
+
checkAndReport(node, node);
|
|
616
|
+
},
|
|
617
|
+
ArrowFunctionExpression(node) {
|
|
618
|
+
if (isCallbackFunction(node)) return;
|
|
619
|
+
checkAndReport(node, node);
|
|
620
|
+
},
|
|
621
|
+
FunctionExpression(node) {
|
|
622
|
+
if (node.parent.type === "MethodDefinition") return;
|
|
623
|
+
if (isCallbackFunction(node)) return;
|
|
624
|
+
checkAndReport(node, node);
|
|
625
|
+
},
|
|
626
|
+
MethodDefinition(node) {
|
|
627
|
+
checkAndReport(node, node.value);
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
}) }
|
|
632
|
+
};
|
|
633
|
+
//#endregion
|
|
634
|
+
//#region src/configs/type-aware.ts
|
|
635
|
+
/**
|
|
636
|
+
* Type-aware rule group: enables `@typescript-eslint`'s `strictTypeChecked`
|
|
637
|
+
* preset, forces type-aware parsing on, and wires the extra type-checked rules
|
|
638
|
+
* plus the custom `no-never-return-type` rule.
|
|
639
|
+
*
|
|
640
|
+
* Ownership (exclusive) — transcribed from `src/eslint.config.mjs` lines 50-58
|
|
641
|
+
* (sliced `strictTypeChecked` + `projectService`), line 66 (`no-never-return`),
|
|
642
|
+
* and lines 112-113 (`use-unknown-in-catch-callback-variable`, `only-throw-error`).
|
|
643
|
+
*
|
|
644
|
+
* `strictTypeChecked[0]` is a pure plugin/parser registration that antfu's own
|
|
645
|
+
* `typescript` config already provides, so it is dropped with `.slice(1)` to
|
|
646
|
+
* avoid a duplicate `@typescript-eslint` registration (which would double every
|
|
647
|
+
* type-checked report). The remaining items reference `@typescript-eslint/*`
|
|
648
|
+
* rules against antfu's registration, so this builder registers only the custom
|
|
649
|
+
* `no-never-return` plugin (antfu does NOT bundle it).
|
|
650
|
+
*
|
|
651
|
+
* `tsconfigRootDir` is deliberately omitted: the copied config pinned it to this
|
|
652
|
+
* package's own directory, which would resolve inside the consumer's
|
|
653
|
+
* `node_modules` once installed and break type-aware linting. Omitting it lets
|
|
654
|
+
* `projectService` default to `process.cwd()` — the consumer's project root.
|
|
655
|
+
*
|
|
656
|
+
* @param _options - Resolved factory options (unused: this group is static).
|
|
657
|
+
* @returns The type-aware flat-config items.
|
|
658
|
+
*/
|
|
659
|
+
function typeAwareConfig(_options) {
|
|
660
|
+
return [
|
|
661
|
+
...tseslint.configs.strictTypeChecked.slice(1),
|
|
662
|
+
{ languageOptions: { parserOptions: { projectService: true } } },
|
|
663
|
+
{
|
|
664
|
+
plugins: { "no-never-return": no_never_return_type_default },
|
|
665
|
+
rules: {
|
|
666
|
+
"no-never-return/no-never-return-type": "error",
|
|
667
|
+
"@typescript-eslint/use-unknown-in-catch-callback-variable": "error",
|
|
668
|
+
"@typescript-eslint/only-throw-error": "error"
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
];
|
|
672
|
+
}
|
|
673
|
+
//#endregion
|
|
674
|
+
//#region src/configs/unicorn.ts
|
|
675
|
+
/**
|
|
676
|
+
* Unicorn rule overrides: catch-block hygiene plus general strictness.
|
|
677
|
+
*
|
|
678
|
+
* Ownership (exclusive) — transcribed from `src/eslint.config.mjs` lines 94-107.
|
|
679
|
+
*
|
|
680
|
+
* antfu already bundles and registers `eslint-plugin-unicorn`, so this builder
|
|
681
|
+
* only overrides rule severities; it does NOT re-register the plugin.
|
|
682
|
+
*
|
|
683
|
+
* @param _options - Resolved factory options (unused: this group is static).
|
|
684
|
+
* @returns The unicorn override flat-config items.
|
|
685
|
+
*/
|
|
686
|
+
function unicornConfig(_options) {
|
|
687
|
+
return [{ rules: {
|
|
688
|
+
"unicorn/catch-error-name": ["error", { name: "error" }],
|
|
689
|
+
"unicorn/prefer-optional-catch-binding": "error",
|
|
690
|
+
"unicorn/throw-new-error": "off",
|
|
691
|
+
"unicorn/consistent-destructuring": "error",
|
|
692
|
+
"unicorn/consistent-function-scoping": "error",
|
|
693
|
+
"unicorn/custom-error-definition": "error",
|
|
694
|
+
"unicorn/no-lonely-if": "error",
|
|
695
|
+
"unicorn/no-nested-ternary": "error",
|
|
696
|
+
"unicorn/no-static-only-class": "error",
|
|
697
|
+
"unicorn/prefer-class-fields": "error"
|
|
698
|
+
} }];
|
|
699
|
+
}
|
|
700
|
+
//#endregion
|
|
701
|
+
//#region src/index.ts
|
|
702
|
+
/**
|
|
703
|
+
* Every opinionated rule-group builder, in the order their items are emitted.
|
|
704
|
+
*
|
|
705
|
+
* Order among *our* blocks is cosmetic (they own disjoint rules per the task's
|
|
706
|
+
* ownership map); what matters is that this whole list is emitted BEFORE any
|
|
707
|
+
* user config — see {@link config}.
|
|
708
|
+
*/
|
|
709
|
+
const ourConfigs = [
|
|
710
|
+
typeAwareConfig,
|
|
711
|
+
sonarjsConfig,
|
|
712
|
+
unicornConfig,
|
|
713
|
+
jsdocConfig,
|
|
714
|
+
namingConfig,
|
|
715
|
+
complexityConfig,
|
|
716
|
+
stylisticConfig,
|
|
717
|
+
promiseConfig,
|
|
718
|
+
customConfig
|
|
719
|
+
];
|
|
720
|
+
/**
|
|
721
|
+
* Normalize the public options into the concrete shape every builder consumes.
|
|
722
|
+
*
|
|
723
|
+
* Applies the {@link DEFAULT_ALIAS} default (`{ prefix: '@', sourceDir: 'src' }`),
|
|
724
|
+
* filling in either field the consumer omitted, and preserves an explicit
|
|
725
|
+
* `alias: false` (which disables the root-alias rule downstream). Pure and total:
|
|
726
|
+
* it reads `options.alias` only and returns a fully-resolved value with no optionals.
|
|
727
|
+
*
|
|
728
|
+
* @param options - The raw factory options (antfu options plus our `alias`).
|
|
729
|
+
* @returns Resolved options with `alias` normalized to `false` or concrete settings.
|
|
730
|
+
*/
|
|
731
|
+
function resolveOptions(options) {
|
|
732
|
+
const { alias } = options;
|
|
733
|
+
if (alias === false) return { alias: false };
|
|
734
|
+
return { alias: {
|
|
735
|
+
prefix: alias?.prefix ?? DEFAULT_ALIAS.prefix,
|
|
736
|
+
sourceDir: alias?.sourceDir ?? DEFAULT_ALIAS.sourceDir
|
|
737
|
+
} };
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Build the agent ESLint flat config: a thin factory over an untouched
|
|
741
|
+
* `@antfu/eslint-config` base with our opinionated rule groups layered on top.
|
|
742
|
+
*
|
|
743
|
+
* Composition is the override guarantee: `antfu(antfuOptions, ...ourItems,
|
|
744
|
+
* ...userConfigs)` emits every one of our blocks BEFORE the user's configs, so
|
|
745
|
+
* any trailing native `{ files, rules }` item wins by last-match-wins. Nothing
|
|
746
|
+
* we ship is locked.
|
|
747
|
+
*
|
|
748
|
+
* @param options - antfu options (passed through unchanged) plus the bespoke
|
|
749
|
+
* `alias` option. The `alias` key is stripped before reaching antfu, since it
|
|
750
|
+
* is not a valid antfu option.
|
|
751
|
+
* @param userConfigs - Native flat-config items appended last to override ours.
|
|
752
|
+
* @returns antfu's `FlatConfigComposer` (supports `.append()`/`.override()`).
|
|
753
|
+
*/
|
|
754
|
+
function config(options = {}, ...userConfigs) {
|
|
755
|
+
const resolved = resolveOptions(options);
|
|
756
|
+
const { alias: _alias, ...antfuOptions } = options;
|
|
757
|
+
return antfu(antfuOptions, ...ourConfigs.flatMap((build) => build(resolved)), ...userConfigs);
|
|
758
|
+
}
|
|
759
|
+
//#endregion
|
|
760
|
+
export { config as default };
|