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/README.md ADDED
@@ -0,0 +1,832 @@
1
+ <p align="center">
2
+ <img src="./docs/assets/eslint-logo-color.svg" width="256px" alt="ESLint logo" />
3
+ </p>
4
+
5
+ <div align="center">
6
+
7
+ <h1>Agent ESLint Config</h1>
8
+
9
+ Overly opinionated ESLint config that forces agents to write low-complexity, highly readable code.
10
+
11
+ [Quick Start](#quick-start) •
12
+ [Customization](#customization) •
13
+ [Included Rules](#included-rules) •
14
+ [Examples](#examples) •
15
+ [Recommendations](#recommendations) •
16
+ [FAQ](#faq)
17
+
18
+ </div>
19
+
20
+ ESLint config preset designed specifically for agents. It provides the strictest possible rules to limit code complexity and security risks, while enforcing best practices and coding standards. Supports Claude, Gemini, Codex, Antigravity, OpenCode, and many more.
21
+
22
+ ## Features
23
+
24
+ - Zero configuration out of the box
25
+ - Highly strict ESLint config that includes rules to limit:
26
+ - The cognitive and cyclomatic complexity of code.
27
+ - The size of functions, files, and classes.
28
+ - The maximum depth of nested `if` statements, loops, and functions.
29
+ - The maximum number of statements, lines, and parameters in a function.
30
+ - Rules enforce:
31
+ - Best practices
32
+ - Coding standards
33
+ - Security checks: avoid common vulnerabilities and security risks
34
+ - Usage of types in functions and classes
35
+ - Minimal JSDoc on all definitions
36
+ - Custom plugins verify code architecture and file-naming conventions: they forbid `util`, `helper`, `common`, and `function` names in files, classes, and functions.
37
+ - Highly customizable and extendable.
38
+ - Based on [`antfu`](https://github.com/antfu/eslint-config), [SonarJS](https://github.com/SonarSource/eslint-plugin-sonarjs), [Unicorn](https://github.com/sindresorhus/eslint-plugin-unicorn), and many more configuration presets and plugins.
39
+ - Auto-fix for the majority of rules.
40
+ - Increases the quality of LLM solutions - after quickly written code is rejected by the config, the LLM usually reflects on its solution and tries to refactor and improve it beyond the config's rules, resulting in better code.
41
+
42
+ ### Style principle
43
+
44
+ *Ease of reading and code maintenance above everything else.*
45
+
46
+ - Single quotes, no semicolons
47
+ - Uses [ESLint Stylistic](https://github.com/eslint-stylistic/eslint-stylistic)
48
+ - Stable diffs: sorted imports, dangling commas
49
+ - Empty lines between statements and blocks
50
+ - Short, single-purpose functions and classes
51
+
52
+ ### Manual usage
53
+
54
+ Our team have been using and testing this config for over a year, over multiple production projects. Empirically, we found that even a slightest decrease in strictness is immediately abused by agents. Resulting in significant code quality regressions. To prevent it, configuration is set so strictly that it allow zero room for misinterpretation, and able to catch bad code in the majority of cases. Unfortunatelly, simultaniusly, writing code by hands becomes quite difficult, but still possible.
55
+
56
+ ## Quick Start
57
+
58
+ Install ESLint and the config:
59
+
60
+ ```bash
61
+ npm install -D eslint agent-eslint-config
62
+ ```
63
+
64
+ Create `eslint.config.mjs` at your project root:
65
+
66
+ ```js
67
+ // eslint.config.mjs
68
+ import config from 'agent-eslint-config'
69
+
70
+ export default config()
71
+ ```
72
+
73
+ Add scripts to `package.json`:
74
+
75
+ ```json
76
+ {
77
+ "scripts": {
78
+ "lint": "eslint",
79
+ "lint:fix": "eslint --fix"
80
+ }
81
+ }
82
+ ```
83
+
84
+ ## Recommendations
85
+
86
+ ### Usage with agents
87
+
88
+ We advice you to use this config together with skills like [`/do-and-judge`](https://neolab.gitbook.io/cek/plugins/sadd/do-and-judge) that forces agents to write code, verify it using linter and then fix it until gate is passed.
89
+
90
+ ### TypeScript configuration
91
+
92
+ To make the alias rule effective and give TypeScript maximum strictness, mirror the alias in your `tsconfig.json` and enable strict compiler options:
93
+
94
+ ```json
95
+ {
96
+ "compilerOptions": {
97
+ "baseUrl": "./",
98
+ "paths": {
99
+ "@/*": ["./src/*"]
100
+ },
101
+ "strict": true,
102
+ "noUncheckedIndexedAccess": true,
103
+ "noImplicitReturns": true,
104
+ "noUnusedLocals": true,
105
+ "noUnusedParameters": true,
106
+ "noFallthroughCasesInSwitch": true,
107
+ "noPropertyAccessFromIndexSignature": false,
108
+ "noImplicitOverride": true,
109
+ "declaration": true,
110
+ "emitDeclarationOnly": true,
111
+ "esModuleInterop": true,
112
+ "isolatedModules": true,
113
+ "verbatimModuleSyntax": true,
114
+ "skipLibCheck": true,
115
+ "allowSyntheticDefaultImports": true,
116
+ "forceConsistentCasingInFileNames": true
117
+ }
118
+ }
119
+ ```
120
+
121
+ ### Code duplication and unused code checks
122
+
123
+ ESLint has a few limitations due to its architecture, which processes each file separately and does not allow cross-file rules. So, to add code-duplication checks you can use `jscpd`, and for unused-code checks you can add `knip`.
124
+
125
+ ```bash
126
+ npm install -D jscpd knip
127
+ ```
128
+
129
+ Create a `knip.json` file:
130
+
131
+ ```json
132
+ {
133
+ "$schema": "https://unpkg.com/knip@5/schema.json",
134
+ "entry": ["src/main.{js,ts}"],
135
+ "project": ["src/**/*.{js,ts}"],
136
+ "tags": ["-lintignore"],
137
+ "rules": {
138
+ "devDependencies": "off",
139
+ "exports": "error",
140
+ "types": "off"
141
+ }
142
+ }
143
+ ```
144
+
145
+ Then include them in your `package.json`:
146
+
147
+ ```json
148
+ {
149
+ "scripts": {
150
+ "lint": "npm run typecheck && npm run lint:jscpd && npm run lint:knip && npm run lint:eslint",
151
+ "lint:fix": "npm run typecheck && npm run lint:jscpd && npm run lint:eslint -- --fix && npm run lint:knip -- --fix",
152
+ "typecheck": "tsc --noEmit",
153
+ "lint:eslint": "eslint \"{src,apps,libs,test}/**/*.ts\"",
154
+ "lint:jscpd": "jscpd --pattern 'src/**/*.{ts,tsx}' -i '**/*.spec.*' -t 0.1",
155
+ "lint:knip": "knip"
156
+ }
157
+ }
158
+ ```
159
+
160
+ ## Examples
161
+
162
+ Examples of code before and after linting. For more examples, see the [`demo/fixtures/`](demo/fixtures/) directory.
163
+
164
+
165
+ ### 1. Decompose a monolithic function
166
+
167
+ Bad code:
168
+
169
+ ```ts
170
+ interface RegisteredUser {
171
+ id: string
172
+ email: string
173
+ passwordHash: string
174
+ }
175
+
176
+ const registry: RegisteredUser[] = []
177
+
178
+ async function processUserRegistration(input: unknown): Promise<RegisteredUser> {
179
+ const data = input as any
180
+ if (!data.email || typeof data.email !== 'string') throw new Error('email is required')
181
+ if (!data.password || typeof data.password !== 'string') throw new Error('password is required')
182
+ const email = data.email.trim().toLowerCase()
183
+ if (!email.includes('@')) throw new Error('invalid email')
184
+ let hash = ''
185
+ for (const character of data.password) {
186
+ if (typeof character === 'string') {
187
+ hash = hash + String(character.charCodeAt(0) * 31 % 255)
188
+ }
189
+ }
190
+ for (const existing of registry) {
191
+ if (existing.email === email) {
192
+ throw new Error('email already registered')
193
+ }
194
+ }
195
+ const user = { id: String(registry.length + 1), email, passwordHash: hash }
196
+ registry.push(user)
197
+ const name = data.name ? String(data.name) : email
198
+ console.error('welcome ' + name)
199
+ console.error('sending confirmation to ' + email)
200
+ await Promise.resolve()
201
+
202
+ return user
203
+ }
204
+ ```
205
+
206
+ Good code:
207
+
208
+ ```ts
209
+ interface RegistrationInput {
210
+ email: string
211
+ password: string
212
+ }
213
+
214
+ interface RegisteredUser {
215
+ id: string
216
+ email: string
217
+ passwordHash: string
218
+ }
219
+
220
+ const registry: RegisteredUser[] = []
221
+
222
+ /**
223
+ * Registers a user: validate, normalize, persist, then notify.
224
+ * @param input The untrusted registration payload.
225
+ * @returns The persisted user record.
226
+ */
227
+ export async function processUserRegistration(input: unknown): Promise<RegisteredUser> {
228
+ const valid = validateRegistrationInput(input)
229
+ const user = normalizeAndHash(valid)
230
+
231
+ await persistUser(user)
232
+ notifyRegistration(user)
233
+
234
+ return user
235
+ }
236
+
237
+ /**
238
+ * Narrows an untrusted payload into a typed registration input.
239
+ * @param input The untrusted registration payload.
240
+ * @returns The validated input.
241
+ */
242
+ function validateRegistrationInput(input: unknown): RegistrationInput {
243
+ if (typeof input !== 'object' || input === null) {
244
+ throw new Error('input must be an object')
245
+ }
246
+
247
+ const record = input as Record<string, unknown>
248
+
249
+ if (typeof record.email !== 'string' || !record.email.includes('@')) {
250
+ throw new Error('a valid email is required')
251
+ }
252
+
253
+ if (typeof record.password !== 'string') {
254
+ throw new TypeError('a password is required')
255
+ }
256
+
257
+ return { email: record.email, password: record.password }
258
+ }
259
+
260
+ /**
261
+ * Normalizes the email and derives a password hash.
262
+ * @param input The validated registration input.
263
+ * @returns A user record ready to persist.
264
+ */
265
+ function normalizeAndHash(input: RegistrationInput): RegisteredUser {
266
+ const email = input.email.trim().toLowerCase()
267
+ const codes = Array.from(input.password, character => character.charCodeAt(0) * 31 % 255)
268
+
269
+ return { id: String(registry.length + 1), email, passwordHash: codes.join('-') }
270
+ }
271
+
272
+ /**
273
+ * Persists a user, rejecting a duplicate email.
274
+ * @param user The user record to store.
275
+ */
276
+ async function persistUser(user: RegisteredUser): Promise<void> {
277
+ const duplicate = registry.some(existing => existing.email === user.email)
278
+
279
+ if (duplicate) {
280
+ throw new Error('email already registered')
281
+ }
282
+
283
+ await Promise.resolve()
284
+ registry.push(user)
285
+ }
286
+
287
+ /**
288
+ * Emits registration notifications for a new user.
289
+ * @param user The freshly registered user.
290
+ */
291
+ function notifyRegistration(user: RegisteredUser): void {
292
+ console.error(`welcome, new user ${user.email}`)
293
+ console.error(`sending confirmation to ${user.email}`)
294
+ }
295
+ ```
296
+
297
+ ### 2. Flatten nested control flow
298
+
299
+ Bad code:
300
+
301
+ ```ts
302
+ interface Account {
303
+ email: string | null
304
+ age: number
305
+ roles: string[]
306
+ }
307
+
308
+ function validateUser(account: Account | null): string {
309
+ if (account !== null) {
310
+ if (account.email !== null) {
311
+ if (account.email.includes('@')) { // unicorn/no-lonely-if: lone `if` inside an `if`
312
+ if (account.age >= 18) {
313
+ if (account.roles.length > 0) {
314
+ return 'ok'
315
+ }
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ return 'invalid'
322
+ }
323
+ ```
324
+
325
+ Good code:
326
+
327
+ ```ts
328
+ interface Account {
329
+ email: string | null
330
+ age: number
331
+ roles: string[]
332
+ }
333
+
334
+ /**
335
+ * Validates an account using flat guard clauses.
336
+ * @param account The account to validate, or null.
337
+ * @returns 'ok' when every requirement is met, otherwise 'invalid'.
338
+ */
339
+ export function validateUser(account: Account | null): string {
340
+ if (account === null) {
341
+ return 'invalid'
342
+ }
343
+
344
+ if (!hasValidEmail(account.email)) {
345
+ return 'invalid'
346
+ }
347
+
348
+ if (account.age < 18) {
349
+ return 'invalid'
350
+ }
351
+
352
+ if (account.roles.length === 0) {
353
+ return 'invalid'
354
+ }
355
+
356
+ return 'ok'
357
+ }
358
+
359
+ /**
360
+ * Reports whether an email is present and well-formed.
361
+ * @param email The email to check, or null.
362
+ * @returns True when the email contains an '@'.
363
+ */
364
+ function hasValidEmail(email: string | null): boolean {
365
+ return email !== null && email.includes('@')
366
+ }
367
+ ```
368
+
369
+ ### 3. Replace a static-only "helper" class
370
+
371
+ Bad code:
372
+
373
+ ```ts
374
+ class SubscriptionHelper {
375
+ static subscribers: string[] = [] // no-restricted-syntax: static property
376
+
377
+ static add(u: string): void { // no-restricted-syntax: static method; id-length: `u`
378
+ SubscriptionHelper.subscribers.push(u)
379
+ }
380
+
381
+ static remove(u: string): void {
382
+ SubscriptionHelper.subscribers = SubscriptionHelper.subscribers.filter(x => x !== u)
383
+ }
384
+
385
+ static count(): number {
386
+ return SubscriptionHelper.subscribers.length
387
+ }
388
+ }
389
+ ```
390
+
391
+ Good code:
392
+
393
+ ```ts
394
+ /** Tracks newsletter subscribers in memory. */
395
+ export class SubscriptionRegistry {
396
+ private readonly subscribers = new Set<string>()
397
+
398
+ /**
399
+ * Adds a subscriber by email.
400
+ * @param email The subscriber email.
401
+ * @returns The subscriber count after adding.
402
+ */
403
+ add(email: string): number {
404
+ this.subscribers.add(email)
405
+
406
+ return this.subscribers.size
407
+ }
408
+
409
+ /**
410
+ * Removes a subscriber by email.
411
+ * @param email The subscriber email.
412
+ * @returns The subscriber count after removing.
413
+ */
414
+ remove(email: string): number {
415
+ this.subscribers.delete(email)
416
+
417
+ return this.subscribers.size
418
+ }
419
+
420
+ /**
421
+ * Counts the current subscribers.
422
+ * @returns The number of subscribers.
423
+ */
424
+ count(): number {
425
+ return this.subscribers.size
426
+ }
427
+ }
428
+ ```
429
+
430
+
431
+ ## Customization
432
+
433
+ Normally you only need to import the preset:
434
+
435
+ ```js
436
+ // eslint.config.js
437
+ import config from 'agent-eslint-config'
438
+
439
+ export default config()
440
+ ```
441
+
442
+ ### Configuring & overriding rules
443
+
444
+ You can also configure each integration individually. The config supports the default [`antfu` options](https://github.com/antfu/eslint-config#customization), plus the one bespoke `alias` option documented below.
445
+
446
+ ```js
447
+ // eslint.config.js
448
+ import config from 'agent-eslint-config'
449
+
450
+ export default config({
451
+ // Disable the alias rule
452
+ alias: false,
453
+
454
+ // Type of the project. 'lib' for libraries, the default is 'app'
455
+ type: 'lib',
456
+
457
+ // `.eslintignore` is no longer supported in Flat config, use `ignores` instead
458
+ // The `ignores` option in the option (first argument) is specifically treated to always be global ignores
459
+ // And will **extend** the config's default ignores, not override them
460
+ // You can also pass a function to modify the default ignores
461
+ ignores: [
462
+ '**/fixtures',
463
+ // ...globs
464
+ ],
465
+
466
+ // Parse the `.gitignore` file to get the ignores, on by default
467
+ gitignore: true,
468
+
469
+ // Enable stylistic formatting rules
470
+ // stylistic: true,
471
+
472
+ // Or customize the stylistic rules
473
+ stylistic: {
474
+ indent: 2, // 4, or 'tab'
475
+ quotes: 'single', // or 'double'
476
+ braceStyle: 'stroustrup', // '1tbs', or 'allman'
477
+ },
478
+
479
+ // TypeScript and Vue are autodetected, you can also explicitly enable them:
480
+ typescript: true,
481
+ vue: true,
482
+
483
+ // Disable jsonc and yaml support
484
+ jsonc: false,
485
+ yaml: false,
486
+ })
487
+ ```
488
+
489
+
490
+ The `config` factory function also accepts any number of arbitrary custom config overrides:
491
+
492
+ ```js
493
+ // eslint.config.js
494
+ import config from 'agent-eslint-config'
495
+
496
+ export default config(
497
+ {
498
+ // Configures for agent-eslint-config
499
+ },
500
+
501
+ // From the second arguments they are ESLint Flat Configs
502
+ // you can have multiple configs
503
+ {
504
+ files: ['**/*.ts'],
505
+ rules: {},
506
+ },
507
+ {
508
+ rules: {},
509
+ },
510
+ )
511
+ ```
512
+
513
+ ### Rules Overrides
514
+
515
+ Certain rules are only enabled in specific files. For example, `ts/*` rules are only enabled in `.ts` files, and `vue/*` rules are only enabled in `.vue` files. If you want to override those rules, you need to specify the file extension:
516
+
517
+ ```js
518
+ // eslint.config.js
519
+ import antfu from '@antfu/eslint-config'
520
+
521
+ export default antfu(
522
+ {
523
+ vue: true,
524
+ typescript: true
525
+ },
526
+ {
527
+ // Remember to specify the file glob here, otherwise it might cause the vue plugin to handle non-vue files
528
+ files: ['**/*.vue'],
529
+ rules: {
530
+ 'vue/operator-linebreak': ['error', 'before'],
531
+ },
532
+ },
533
+ {
534
+ // Without `files`, they are general rules for all files (Markdown excluded — see note below)
535
+ rules: {
536
+ 'style/semi': ['error', 'never'],
537
+ },
538
+ }
539
+ )
540
+ ```
541
+
542
+ ### Config Composer
543
+
544
+ `config()` returns a [composer object](https://github.com/antfu/eslint-flat-config-utils#composer) whose methods you can chain to compose the config even more flexibly:
545
+
546
+ ```js
547
+ // eslint.config.js
548
+ import config from 'agent-eslint-config'
549
+
550
+ export default config()
551
+ .prepend(
552
+ // some configs before the main config
553
+ )
554
+ // overrides any named configs
555
+ .override(
556
+ 'antfu/stylistic/rules',
557
+ {
558
+ rules: {
559
+ 'style/generator-star-spacing': ['error', { after: true, before: false }],
560
+ }
561
+ }
562
+ )
563
+ // rename plugin prefixes
564
+ .renamePlugins({
565
+ 'old-prefix': 'new-prefix',
566
+ // ...
567
+ })
568
+ // ...
569
+ ```
570
+
571
+ ### The `alias` option
572
+
573
+ The `alias` option configures the custom `prefer-alias` rule, which rewrites relative imports that reach into your source directory (e.g. `../services/user`) into an aliased form (e.g. `@/services/user`).
574
+
575
+ | Value | Effect |
576
+ | --- | --- |
577
+ | _omitted_ (default) | `{ prefix: '@', sourceDir: 'src' }` |
578
+ | `{ prefix?, sourceDir? }` | Customize either field; any omitted field falls back to the default above |
579
+ | `false` | Disable the `prefer-alias` rule entirely |
580
+
581
+ ```js
582
+ import config from 'agent-eslint-config'
583
+
584
+ // Default: '@' maps to 'src'
585
+ export default config()
586
+
587
+ // Custom prefix/sourceDir
588
+ export default config({ alias: { prefix: '~', sourceDir: 'app' } })
589
+
590
+ // Disable the alias rule
591
+ export default config({ alias: false })
592
+ ```
593
+
594
+ ## Included Rules
595
+
596
+ Layered on top of the full `@antfu/eslint-config` base, this package explicitly configures **82 rules** across nine groups (plus the layered `@typescript-eslint` `strictTypeChecked` preset). Every rule listed below is `error`-level and overridable via the customization mechanisms above. These rules are emitted *before* any user config, so your own `{ files, rules }` overrides always win last.
597
+
598
+ > **Prefix note.** Every `@typescript-eslint/*` rule is emitted under the `ts/` prefix, because `antfu` registers the `typescript-eslint` plugin under the `ts` namespace. Use `ts/`, not `@typescript-eslint/`, in your overrides.
599
+
600
+ ### Complexity
601
+
602
+ Aggressive size and complexity thresholds from ESLint core plus SonarJS.
603
+
604
+ | Rule | Description |
605
+ | --- | --- |
606
+ | `complexity` | Cyclomatic complexity ≤ 10 per function (`[error, 10]`). |
607
+ | `max-depth` | Block nesting depth ≤ 2 (`[error, 2]`). |
608
+ | `max-lines-per-function` | ≤ 40 code lines per function (skips blanks/comments). |
609
+ | `max-statements` | ≤ 10 statements per function (`[error, 10]`). |
610
+ | `max-lines` | ≤ 150 code lines per file (skips blanks/comments). |
611
+ | `max-nested-callbacks` | Callback nesting depth ≤ 3 (`[error, 3]`). |
612
+ | `max-params` | ≤ 3 function parameters (`[error, 3]`). |
613
+ | `sonarjs/cognitive-complexity` | Cognitive complexity ≤ 4 per function (`[error, 4]`). |
614
+
615
+ ### SonarJS
616
+
617
+ 35 rules from `eslint-plugin-sonarjs`, grouped by concern. (The three SonarJS naming rules live in the Naming group and `sonarjs/cognitive-complexity` lives in the Complexity group.)
618
+
619
+ **Control flow**
620
+
621
+ | Rule | Description |
622
+ | --- | --- |
623
+ | `sonarjs/nested-control-flow` | Limits nesting depth of control-flow statements to 2. |
624
+ | `sonarjs/too-many-break-or-continue-in-loop` | Forbids multiple `break`/`continue` in a loop. |
625
+ | `sonarjs/elseif-without-else` | Requires a closing `else` after an `else if` chain. |
626
+ | `sonarjs/no-nested-conditional` | Forbids nested ternary/conditional expressions. |
627
+ | `sonarjs/no-same-line-conditional` | Forbids conditionals sharing a line. |
628
+ | `sonarjs/conditional-indentation` | Enforces consistent indentation of conditionals. |
629
+
630
+ **Dead code & redundancy**
631
+
632
+ | Rule | Description |
633
+ | --- | --- |
634
+ | `sonarjs/no-all-duplicated-branches` | Forbids conditionals whose branches are all identical. |
635
+ | `sonarjs/no-duplicated-branches` | Forbids duplicated branches in conditionals/switch. |
636
+ | `sonarjs/no-dead-store` | Forbids assignments whose value is never read. |
637
+ | `sonarjs/no-redundant-assignments` | Forbids assignments that duplicate the existing value. |
638
+ | `sonarjs/no-identical-functions` | Forbids duplicate function bodies (≥ 3 lines). |
639
+ | `sonarjs/no-useless-catch` | Forbids `catch` blocks that only rethrow. |
640
+ | `sonarjs/no-useless-increment` | Forbids increments whose result is unused. |
641
+ | `sonarjs/useless-string-operation` | Forbids no-op string operations. |
642
+ | `sonarjs/prefer-immediate-return` | Prefers returning an expression over assign-then-return. |
643
+
644
+ **Nesting & assignments**
645
+
646
+ | Rule | Description |
647
+ | --- | --- |
648
+ | `sonarjs/no-nested-assignment` | Forbids assignments nested inside expressions. |
649
+ | `sonarjs/no-nested-functions` | Forbids deeply nested function declarations. |
650
+ | `sonarjs/no-nested-incdec` | Forbids nested increment/decrement. |
651
+ | `sonarjs/no-parameter-reassignment` | Forbids reassigning function parameters. |
652
+ | `sonarjs/destructuring-assignment-syntax` | Enforces destructuring assignment syntax. |
653
+
654
+ **Loops**
655
+
656
+ | Rule | Description |
657
+ | --- | --- |
658
+ | `sonarjs/misplaced-loop-counter` | Forbids updating the wrong counter in a loop. |
659
+ | `sonarjs/updated-loop-counter` | Forbids mutating a loop counter in the body. |
660
+
661
+ **Functions & declarations**
662
+
663
+ | Rule | Description |
664
+ | --- | --- |
665
+ | `sonarjs/no-function-declaration-in-block` | Forbids function declarations inside blocks. |
666
+ | `sonarjs/no-globals-shadowing` | Forbids shadowing global identifiers. |
667
+ | `sonarjs/no-fallthrough` | Forbids switch-case fallthrough. |
668
+ | `sonarjs/no-reference-error` | Flags likely `ReferenceError`s (use-before-define). |
669
+ | `sonarjs/no-unthrown-error` | Flags `Error` objects created but never thrown. |
670
+ | `sonarjs/prefer-type-guard` | Prefers type-guard functions over inline type checks. |
671
+
672
+ **Promises & async**
673
+
674
+ | Rule | Description |
675
+ | --- | --- |
676
+ | `sonarjs/no-try-promise` | Forbids `try/catch` around a Promise without `await`. |
677
+
678
+ **Security**
679
+
680
+ | Rule | Description |
681
+ | --- | --- |
682
+ | `sonarjs/no-hardcoded-ip` | Forbids hardcoded IP addresses. |
683
+ | `sonarjs/no-hardcoded-passwords` | Forbids hardcoded passwords. |
684
+ | `sonarjs/no-hardcoded-secrets` | Forbids hardcoded secrets/tokens. |
685
+ | `sonarjs/os-command` | Flags risky OS command execution. |
686
+
687
+ **Testing**
688
+
689
+ | Rule | Description |
690
+ | --- | --- |
691
+ | `sonarjs/no-skipped-tests` | Forbids skipped tests (`.skip`). |
692
+ | `sonarjs/stable-tests` | Forbids unstable/non-deterministic test patterns. |
693
+
694
+ ### Unicorn
695
+
696
+ 10 rules overridden on `eslint-plugin-unicorn` (registered by `antfu`).
697
+
698
+ | Rule | Description |
699
+ | --- | --- |
700
+ | `unicorn/catch-error-name` | Requires the caught error variable to be named `error` (`{ name: 'error' }`). |
701
+ | `unicorn/prefer-optional-catch-binding` | Prefers omitting the catch binding when it is unused. |
702
+ | `unicorn/consistent-destructuring` | Requires consistent destructuring of an object. |
703
+ | `unicorn/consistent-function-scoping` | Moves functions to the outermost scope that works. |
704
+ | `unicorn/custom-error-definition` | Enforces correct custom `Error` subclass definitions. |
705
+ | `unicorn/no-lonely-if` | Forbids an `if` as the only statement inside an `else`. |
706
+ | `unicorn/no-nested-ternary` | Forbids nested ternary expressions. |
707
+ | `unicorn/no-static-only-class` | Forbids classes containing only static members. |
708
+ | `unicorn/prefer-class-fields` | Prefers class fields over constructor assignment. |
709
+ | `unicorn/throw-new-error` | **Turned OFF** (conflicts with catch decorators). |
710
+
711
+ ### Type-aware
712
+
713
+ Enables `@typescript-eslint`'s `strictTypeChecked` preset (emitted under the `ts/` prefix), which requires a resolvable `tsconfig.json`. On top of the preset, the package explicitly configures the following:
714
+
715
+ | Rule | Description |
716
+ | --- | --- |
717
+ | `no-never-return/no-never-return-type` | Bans functions whose resolved return type is `never` (throw-only wrappers); throw at the call site instead. Type-aware; ignores callback functions. |
718
+ | `ts/use-unknown-in-catch-callback-variable` | Forces `unknown` typing for the parameter of `.catch()` / promise-rejection callbacks. |
719
+ | `ts/only-throw-error` | Disallows throwing values that are not `Error` objects. |
720
+
721
+ **The `strictTypeChecked` preset.** This package enables the entire `@typescript-eslint` `strictTypeChecked` preset (~72 enabled type-aware rules, all emitted as `ts/*`). The preset also turns **off ~28 core ESLint rules** it supersedes with type-aware equivalents (e.g. core `no-throw-literal`, `no-unused-vars`, `require-await`, `no-implied-eval` — use the `ts/*` versions instead), in addition to the 5 `antfu`-enabled rules listed under [Rules deliberately turned off](#rules-deliberately-turned-off). The preset is not enumerated in full here because its exact membership is version-dependent, but notable rules it brings in include:
722
+
723
+ - `ts/no-explicit-any`, `ts/no-unsafe-argument`, `ts/no-unsafe-assignment`, `ts/no-unsafe-call`, `ts/no-unsafe-member-access`, `ts/no-unsafe-return`
724
+ - `ts/no-floating-promises`, `ts/no-misused-promises`, `ts/await-thenable`, `ts/require-await`
725
+ - `ts/no-unnecessary-condition`, `ts/no-unnecessary-type-assertion`, `ts/no-non-null-assertion`
726
+ - `ts/restrict-template-expressions`, `ts/restrict-plus-operands`, `ts/no-base-to-string`
727
+ - `ts/unbound-method`, `ts/no-confusing-void-expression`, `ts/ban-ts-comment`
728
+
729
+ The following notable type-checked rules are configured or emphasized by this package (the last two are set in the Stylistic group but still require type information): `ts/use-unknown-in-catch-callback-variable`, `ts/only-throw-error`, `ts/consistent-type-definitions`, `ts/class-methods-use-this`.
730
+
731
+ ### Naming
732
+
733
+ 5 rules from `eslint-plugin-sonarjs` and `eslint-plugin-validate-filename`. All ban the vague terms `util`, `common`, `helper`, and `function` (in any case).
734
+
735
+ | Rule | Description |
736
+ | --- | --- |
737
+ | `validate-filename/naming-rules` | Forbids `util`/`common`/`helper`/`function` in `*.ts` file names (glob-scoped to `**/*.ts`). |
738
+ | `sonarjs/class-name` | Class names must be PascalCase and must not contain the vague terms. |
739
+ | `sonarjs/function-name` | Function names must be camelCase/PascalCase and must not contain the vague terms. |
740
+ | `sonarjs/variable-name` | Variable names must be camelCase/PascalCase/UPPER_SNAKE and must not contain the vague terms. |
741
+ | `test/prefer-lowercase-title` | **Turned OFF** (disables `antfu`'s lowercase test-title default). |
742
+
743
+ ### Stylistic
744
+
745
+ 12 rules from ESLint core, `@typescript-eslint` (emitted as `ts/*`), and `perfectionist`.
746
+
747
+ | Rule | Description |
748
+ | --- | --- |
749
+ | `ts/consistent-type-definitions` | Requires `interface` over `type` for object types (`[error, 'interface']`). |
750
+ | `ts/class-methods-use-this` | Requires class methods to use `this` (with override/interface exceptions). |
751
+ | `no-warning-comments` | Forbids `jscpd:ignore-*` marker comments. |
752
+ | `prefer-const` | Requires `const` where a binding is never reassigned. |
753
+ | `init-declarations` | Requires variables to be initialized at declaration (`[error, 'always']`). |
754
+ | `id-length` | Identifier length must be 3–35 characters, with exceptions (`i`, `j`, `k`, `x`, `y`, `z`, `_`, `id`, `on`, `in`, `of`). |
755
+ | `padding-line-between-statements` | Requires blank lines after `const`/`let` declarations, before `return`, and around control-flow blocks. |
756
+ | `preserve-caught-error` | Requires preserving the original caught error (`cause`) when rethrowing. |
757
+ | `no-restricted-syntax` | Bans static methods and static properties (use instance members). |
758
+ | `ts/consistent-type-imports` | **Turned OFF** (NestJS DI needs value imports). |
759
+ | `perfectionist/sort-named-imports` | **Turned OFF** (do not sort named imports). |
760
+ | `class-methods-use-this` | **Turned OFF** (replaced by the `ts/` variant above). |
761
+
762
+ ### Promise
763
+
764
+ 1 rule from `eslint-plugin-promise`.
765
+
766
+ | Rule | Description |
767
+ | --- | --- |
768
+ | `promise/prefer-await-to-then` | Prefers `await` over `.then()`/`.catch()` chaining. |
769
+
770
+ ### JSDoc
771
+
772
+ 6 rules overridden on `eslint-plugin-jsdoc` (registered by `antfu`).
773
+
774
+ | Rule | Description |
775
+ | --- | --- |
776
+ | `jsdoc/require-jsdoc` | Requires JSDoc on function/method/class declarations, constructors, getters, and setters (not on arrows or function expressions). |
777
+ | `jsdoc/require-description` | Requires a description in JSDoc blocks. |
778
+ | `jsdoc/require-param` | Requires a `@param` for each parameter. |
779
+ | `jsdoc/require-returns` | Requires a `@returns` for functions that return a value. |
780
+ | `jsdoc/check-param-names` | Requires `@param` names to match the signature. |
781
+ | `jsdoc/no-blank-blocks` | Forbids empty JSDoc blocks. |
782
+
783
+ ### Custom
784
+
785
+ Rules implemented by this package's own plugins.
786
+
787
+ | Rule | Fixable | Description |
788
+ | --- | --- | --- |
789
+ | `step-down-rule/step-down` | No | Enforces top-down call structure — callers appear before callees. Decorator factories defined after use are allowed. |
790
+ | `alias/prefer-alias` | Yes (`code`) | Rewrites relative imports that reach into the source dir (`../foo`) to the alias form (`@/foo`). Option-gated: omitted entirely when `config({ alias: false })`. |
791
+ | `no-never-return/no-never-return-type` | No | Bans functions whose resolved return type is `never`. Type-aware; also listed under [Type-aware](#type-aware) above. |
792
+
793
+ ## FAQ
794
+
795
+ ### How to disable type-aware linting
796
+
797
+ This linter includes a type-aware layer — `@typescript-eslint`'s `strictTypeChecked` preset, extra type-checked rules, and the custom `no-never-return-type` rule. As a result, it requires a resolvable `tsconfig.json` in the project root.
798
+
799
+ If you have files without a resolvable `tsconfig.json`:
800
+
801
+ 1. **Preferred** — add a `tsconfig.json` at your project root that includes those files. This is a one-line fix for most projects and unlocks the type-aware rules.
802
+ 2. **Otherwise** — append a trailing config item (which wins by the ordering guarantee) that turns type-aware parsing **and** every type-checked rule off for the affected globs.
803
+
804
+ Turning off `projectService` alone is **not** enough: the type-aware layer emits its type-checked rules *globally* (with no `files` restriction), so those rules would still run against the untyped files and throw _"You have used a rule which requires type information …"_ errors. You must also disable the type-checked rules for the same glob. `typescript-eslint`'s `disableTypeChecked.rules` switches off the whole `@typescript-eslint` type-checked set (the `strictTypeChecked` preset plus `use-unknown-in-catch-callback-variable` and `only-throw-error`) in one spread; then disable the one custom type-aware rule, which is not part of that preset:
805
+
806
+ ```js
807
+ // eslint.config.mjs
808
+ import config from 'agent-eslint-config'
809
+ import tseslint from 'typescript-eslint' // shipped as a dependency of this package
810
+
811
+ export default config(
812
+ {},
813
+ {
814
+ files: ['scripts/**/*.js'],
815
+ // Stop resolving type information for these files.
816
+ languageOptions: {
817
+ parserOptions: { projectService: false },
818
+ },
819
+ rules: {
820
+ // Turn off the full @typescript-eslint type-checked rule set
821
+ // (strictTypeChecked + use-unknown-in-catch-callback-variable +
822
+ // only-throw-error) so none of them demand parser services here.
823
+ ...tseslint.configs.disableTypeChecked.rules,
824
+ // The custom type-aware rule is not part of disableTypeChecked, so
825
+ // turn it off explicitly.
826
+ 'no-never-return/no-never-return-type': 'off',
827
+ },
828
+ },
829
+ )
830
+ ```
831
+
832
+ > If your package manager isolates transitive dependencies (e.g. pnpm's strict `node_modules`) and the `typescript-eslint` import does not resolve, add it directly with `npm install -D typescript-eslint`.