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/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 };