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/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`.
|