@skapxd/eslint-opinionated 0.13.0 → 0.15.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 +862 -0
- package/dist/astro/index.d.mts +14 -0
- package/dist/astro/index.d.ts +14 -0
- package/dist/astro/index.js +10 -0
- package/dist/astro/index.js.map +1 -1
- package/dist/astro/index.mjs +2 -2
- package/dist/{chunk-JA2ZO3KQ.mjs → chunk-DJDXQWCP.mjs} +2 -2
- package/dist/{chunk-QADXO5IL.mjs → chunk-F6GJW5A4.mjs} +61 -3
- package/dist/chunk-F6GJW5A4.mjs.map +1 -0
- package/dist/{chunk-UXF7WZ5B.mjs → chunk-MLFXSEY7.mjs} +1705 -76
- package/dist/chunk-MLFXSEY7.mjs.map +1 -0
- package/dist/chunk-O5BXMSNR.mjs +140 -0
- package/dist/chunk-O5BXMSNR.mjs.map +1 -0
- package/dist/{chunk-DJXM5PMG.mjs → chunk-OVC5GQV3.mjs} +2 -2
- package/dist/index.js +1908 -90
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10 -5
- package/dist/index.mjs.map +1 -1
- package/dist/nest/index.d.mts +112 -0
- package/dist/nest/index.d.ts +112 -0
- package/dist/nest/index.js +250 -0
- package/dist/nest/index.js.map +1 -0
- package/dist/nest/index.mjs +8 -0
- package/dist/nest/index.mjs.map +1 -0
- package/dist/next/index.d.mts +14 -0
- package/dist/next/index.d.ts +14 -0
- package/dist/next/index.js +10 -0
- package/dist/next/index.js.map +1 -1
- package/dist/next/index.mjs +2 -2
- package/dist/shared/index.d.mts +43 -0
- package/dist/shared/index.d.ts +43 -0
- package/dist/shared/index.js +1797 -111
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +2 -2
- package/package.json +6 -1
- package/dist/chunk-QADXO5IL.mjs.map +0 -1
- package/dist/chunk-UXF7WZ5B.mjs.map +0 -1
- /package/dist/{chunk-JA2ZO3KQ.mjs.map → chunk-DJDXQWCP.mjs.map} +0 -0
- /package/dist/{chunk-DJXM5PMG.mjs.map → chunk-OVC5GQV3.mjs.map} +0 -0
package/README.md
CHANGED
|
@@ -90,6 +90,28 @@ Quiero que un agente pueda generar código, pero que el proyecto le conteste:
|
|
|
90
90
|
|
|
91
91
|
Eso es lo que estas reglas intentan proteger.
|
|
92
92
|
|
|
93
|
+
## Los axiomas
|
|
94
|
+
|
|
95
|
+
Las reglas no son una colección de gustos: se derivan de ocho axiomas. Si una
|
|
96
|
+
regla nueva no es consecuencia de alguno, no entra. Si dos reglas chocan, gana
|
|
97
|
+
la que defiende el axioma más fundamental (el orden es jerárquico).
|
|
98
|
+
|
|
99
|
+
| # | Axioma | Reglas que lo ejecutan |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| A1 | **Los estados imposibles son irrepresentables.** El tipo modela exactamente los estados válidos; lo inválido no compila. | `prefer-tagged-union-state`, `no-runtime-state-guard`, `requires-strict-tsconfig`, `no-impossible-branch`, `no-explicit-any`, `consistent-type-definitions` |
|
|
102
|
+
| A2 | **Ningún efecto es invisible al tipo.** Si una operación puede fallar, su firma lo confiesa — no una convención oral ni un `throw` sorpresa. | `await-requires-result`, `no-try-catch`, `no-promise-chain`, `no-ad-hoc-ok-result`, `@typescript-eslint/no-floating-promises` |
|
|
103
|
+
| A3 | **La información no se destruye.** Un error que se transforma conserva su `cause`; uno que se detecta llega a alguien. Nadie decide "esto no importa" en silencio. | `result-error-requires-cause`, `result-error-requires-handling` |
|
|
104
|
+
| A4 | **Una unidad, una responsabilidad, un nombre semántico.** El árbol de archivos cuenta una historia; una clase expone una intención. | `one-root-function-per-file`, `max-public-methods`, `no-default-export`, `jsx-return-name-pascal-case`, `max-hook-size` |
|
|
105
|
+
| A5 | **Las decisiones se declaran, no se interpretan.** Cada rama es explícita y exhaustiva; un caso ignorado es una decisión visible, no un hueco. | `no-else`, `no-nested-if`, `prefer-ts-pattern`, `@typescript-eslint/ban-ts-comment`, el `void promesa()` de `no-floating-promises` |
|
|
106
|
+
| A6 | **Evidencia sobre convención.** Una regla decide por lo que el type-checker o los imports demuestran, no por cómo se llama un archivo o un campo. | la implementación type-aware de las reglas de Result, `nest-no-direct-instantiation` (@Injectable resuelto por símbolo), la exención ORM por decorador de `class-properties-require-readonly` |
|
|
107
|
+
| A7 | **Las fronteras son explícitas y únicas.** Lo que cruza una capa lo hace por un contrato, una sola vez, sin túneles. | `no-deep-relative-imports`, `no-tunnel-props`, `nest-no-result-response`, `nest-no-swagger-in-controllers`, `nest-no-inline-query-params` |
|
|
108
|
+
| A8 | **Inmutable por defecto.** La mutación es la excepción que se pide con evidencia, no el estado natural de las cosas. | `class-properties-require-readonly`, `no-accessors` |
|
|
109
|
+
|
|
110
|
+
A6 es distinto a los demás: no produce reglas, produce **cómo se implementan**
|
|
111
|
+
todas. Por eso las reglas de este paquete prefieren parser services y
|
|
112
|
+
provenance de imports antes que globs de nombres — el nombre es la evidencia
|
|
113
|
+
más débil que aceptamos, y solo como último recurso.
|
|
114
|
+
|
|
93
115
|
## Por qué las alternativas no bastan
|
|
94
116
|
|
|
95
117
|
### ESLint core
|
|
@@ -252,6 +274,149 @@ hay errores, sale con código `1` (apto para CI). Como acota por **archivo
|
|
|
252
274
|
completo**, también dispara las reglas estructurales (p. ej.
|
|
253
275
|
`one-root-function-per-file`), que un filtrado por línea se perdería.
|
|
254
276
|
|
|
277
|
+
## Adopción en proyectos legacy: de `off` a `error`, por olas
|
|
278
|
+
|
|
279
|
+
El CLI de arriba acota **qué archivos** se juzgan. Este apartado acota **qué
|
|
280
|
+
reglas** — el camino para meter el preset completo en un proyecto legacy
|
|
281
|
+
escrito por humanos, sin que el primer `pnpm lint` escupa 2.000 errores y el
|
|
282
|
+
equipo apague el linter para siempre.
|
|
283
|
+
|
|
284
|
+
### Las reglas del juego
|
|
285
|
+
|
|
286
|
+
1. **`off` o `error`, nunca `warn`.** Un warn se ignora desde el día dos y
|
|
287
|
+
solo entrena al equipo a ignorar amarillo. Una regla está adoptada
|
|
288
|
+
(`error`) o todavía no (`off`) — no hay estado intermedio.
|
|
289
|
+
2. **Una regla a la vez, y a cero.** Se activa una regla, se arreglan TODOS
|
|
290
|
+
sus hallazgos, se mergea en verde. Nunca actives una regla con pendientes:
|
|
291
|
+
el CI rojo permanente es la ventana rota que normaliza ignorar el linter.
|
|
292
|
+
3. **Ratchet: lo que se enciende no se apaga.** El bloque de `off` solo puede
|
|
293
|
+
encoger. El diff de ese bloque ES la métrica de progreso del equipo.
|
|
294
|
+
4. **Mide antes de activar.** En una rama, borra el `off` de una regla y corre
|
|
295
|
+
el lint: el número de hallazgos es el precio. **Activa siempre la más
|
|
296
|
+
barata pendiente** — el momentum importa más que el orden perfecto.
|
|
297
|
+
5. **Deja que el mensaje enseñe.** Los mensajes de error de estas reglas
|
|
298
|
+
explican el porqué y el cómo (qué patrón usar, cómo se llama, dónde va).
|
|
299
|
+
Para un equipo sin seniors, el linter es el code review que nadie tiene
|
|
300
|
+
tiempo de hacer: no resumas las reglas en un documento aparte — el
|
|
301
|
+
documento es el error en pantalla.
|
|
302
|
+
|
|
303
|
+
### El mecanismo: la lista de pendientes
|
|
304
|
+
|
|
305
|
+
El preset completo es la meta; un bloque posterior apaga lo que el equipo aún
|
|
306
|
+
no cumple. **Adoptar una regla = borrar su línea y arreglar lo que aflore:**
|
|
307
|
+
|
|
308
|
+
```js
|
|
309
|
+
// eslint.config.js
|
|
310
|
+
import skapxd from "@skapxd/eslint-opinionated";
|
|
311
|
+
|
|
312
|
+
export default [
|
|
313
|
+
...skapxd.configs.nest, // la meta: el preset completo, desde el día uno
|
|
314
|
+
|
|
315
|
+
// ─── Lista de pendientes ───────────────────────────────────────────
|
|
316
|
+
// Todo lo que el proyecto aún no cumple, apagado y a la vista.
|
|
317
|
+
// Este bloque SOLO ENCOGE: se borra una línea, se arregla, se mergea.
|
|
318
|
+
{
|
|
319
|
+
rules: {
|
|
320
|
+
"skapxd/await-requires-result": "off",
|
|
321
|
+
"skapxd/no-try-catch": "off",
|
|
322
|
+
// ...
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
];
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### El orden de las olas
|
|
329
|
+
|
|
330
|
+
El orden no es arbitrario: va de "cada hallazgo es un bug que ya tienes" hacia
|
|
331
|
+
"esto exige rediseñar tipos", y cada ola deja el suelo que la siguiente pisa.
|
|
332
|
+
|
|
333
|
+
**Ola 1 — bugs gratis y fixes únicos.** Señal pura, arreglo puntual, cero
|
|
334
|
+
rediseño. Aquí el equipo aprende que el linter encuentra cosas reales:
|
|
335
|
+
|
|
336
|
+
- `@typescript-eslint/no-floating-promises` — cada hallazgo es un error que
|
|
337
|
+
hoy muere sin que nadie lo vea (en un backend real en producción: 12).
|
|
338
|
+
- `skapxd/nest-requires-swagger-plugin` y `skapxd/nest-validation-pipe-config`
|
|
339
|
+
— un hallazgo por proyecto, un fix de configuración, y quedan vigiladas las
|
|
340
|
+
premisas de las olas siguientes.
|
|
341
|
+
- `skapxd/requires-strict-tsconfig` con la exigencia mínima:
|
|
342
|
+
`{ requiredCompilerOptions: ["strict"] }`. Es el trinquete del tsconfig —
|
|
343
|
+
cada ola le sube un flag (ver abajo).
|
|
344
|
+
- `skapxd/no-emoji`, `skapxd/no-deep-relative-imports` — fixes mecánicos.
|
|
345
|
+
- `skapxd/prefer-abort-signal` (front) — cada hallazgo es un leak.
|
|
346
|
+
|
|
347
|
+
**Ola 2 — la forma del código.** Refactors locales, archivo por archivo, sin
|
|
348
|
+
tocar contratos. Es la ola que más enseña por repetición:
|
|
349
|
+
|
|
350
|
+
- `skapxd/no-nested-if` y `skapxd/no-else` — guard clauses. El refactor más
|
|
351
|
+
formativo que existe para un junior: aplana la lógica o confiesa que la
|
|
352
|
+
función hace demasiado.
|
|
353
|
+
- `skapxd/one-root-function-per-file` y `skapxd/no-default-export` — el árbol
|
|
354
|
+
de archivos empieza a contar la historia.
|
|
355
|
+
- `skapxd/no-accessors`, `skapxd/max-public-methods` — clases con una
|
|
356
|
+
intención (partir un god-object es la cirugía mayor de esta ola: déjala de
|
|
357
|
+
última).
|
|
358
|
+
- Front: `skapxd/jsx-return-name-pascal-case`, `skapxd/max-hook-size`,
|
|
359
|
+
`skapxd/no-functions-inside-components`, `skapxd/no-jsx-ternary-null`,
|
|
360
|
+
`skapxd/no-tunnel-props`.
|
|
361
|
+
- Nest: `skapxd/nest-no-swagger-in-controllers`,
|
|
362
|
+
`skapxd/nest-dto-requires-api-property`,
|
|
363
|
+
`skapxd/nest-dto-requires-validation`,
|
|
364
|
+
`skapxd/nest-no-inline-query-params`,
|
|
365
|
+
`skapxd/nest-no-direct-instantiation` — mover decoradores y dependencias a
|
|
366
|
+
donde pertenecen.
|
|
367
|
+
|
|
368
|
+
**Ola 3 — el contrato de errores.** La migración de paradigma
|
|
369
|
+
(`@skapxd/result` + `ts-pattern`; ver "Cómo encaja todo" abajo). Aquí NO se va
|
|
370
|
+
regla por regla sino **módulo por módulo**: las seis reglas entran juntas
|
|
371
|
+
(son un solo sistema) pero acotadas por carpeta, y el primer módulo migrado se
|
|
372
|
+
vuelve el ejemplo canónico que el resto copia:
|
|
373
|
+
|
|
374
|
+
```js
|
|
375
|
+
// Ola 3: el pipeline de Result entra carpeta por carpeta.
|
|
376
|
+
{
|
|
377
|
+
files: ["src/modules/payments/**"],
|
|
378
|
+
rules: {
|
|
379
|
+
"skapxd/await-requires-result": "error",
|
|
380
|
+
"skapxd/no-try-catch": "error",
|
|
381
|
+
"skapxd/no-promise-chain": "error",
|
|
382
|
+
"skapxd/no-ad-hoc-ok-result": "error",
|
|
383
|
+
"skapxd/prefer-ts-pattern": "error",
|
|
384
|
+
"skapxd/result-error-requires-cause": "error",
|
|
385
|
+
"skapxd/result-error-requires-handling": "error",
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
(En Nest, suma `skapxd/nest-no-result-response` al grupo: el controller del
|
|
391
|
+
módulo migrado traduce el Result, no lo serializa.) Cuando todos los módulos
|
|
392
|
+
migraron, las líneas salen del bloque por-carpeta y entran globales: se borran
|
|
393
|
+
de la lista de pendientes.
|
|
394
|
+
|
|
395
|
+
**Ola 4 — el modelado de estados.** Lo más profundo: exige criterio de
|
|
396
|
+
diseño, no solo disciplina. Para cuando el equipo ya vio el patrón en la ola 3:
|
|
397
|
+
|
|
398
|
+
- `requires-strict-tsconfig` al máximo: `["strict", "noImplicitReturns",
|
|
399
|
+
"noUncheckedIndexedAccess"]`. Sube un flag a la vez — cada uno aflora
|
|
400
|
+
errores de compilación que son bugs latentes, no burocracia.
|
|
401
|
+
- `@typescript-eslint/no-explicit-any`, `no-non-null-assertion` y
|
|
402
|
+
`ban-ts-comment` — se cierran las tres puertas de escape del compilador.
|
|
403
|
+
- `skapxd/class-properties-require-readonly` — el cambio se modela con
|
|
404
|
+
instancias nuevas.
|
|
405
|
+
- `skapxd/prefer-tagged-union-state` y `skapxd/no-runtime-state-guard` — los
|
|
406
|
+
booleanos co-dependientes se vuelven uniones etiquetadas.
|
|
407
|
+
- `skapxd/no-impossible-branch` — **la última de todas**: solo es sólida
|
|
408
|
+
cuando el tsconfig ya está al máximo (sin `noUncheckedIndexedAccess`,
|
|
409
|
+
acusaría guards necesarios).
|
|
410
|
+
|
|
411
|
+
### Los dos ejes se combinan
|
|
412
|
+
|
|
413
|
+
Mientras la lista de pendientes encoge, `skapxd-lint-changed` aplica lo ya
|
|
414
|
+
activado solo a los archivos tocados: el código nuevo nace cumpliendo y el
|
|
415
|
+
legacy se corrige cuando alguien lo visita (regla del boy scout), no en un
|
|
416
|
+
big-bang. Un proyecto mediano recorre las cuatro olas en semanas, no en
|
|
417
|
+
trimestres — y cada semana el lint encuentra menos, porque el equipo ya
|
|
418
|
+
escribe distinto.
|
|
419
|
+
|
|
255
420
|
## Cómo encaja todo: `@skapxd/result` + `ts-pattern`
|
|
256
421
|
|
|
257
422
|
Este plugin no es una colección de reglas sueltas: es el guardián de un
|
|
@@ -353,6 +518,9 @@ src/
|
|
|
353
518
|
│ ├── rules.ts
|
|
354
519
|
│ ├── configs/
|
|
355
520
|
│ └── index.ts
|
|
521
|
+
├── nest/
|
|
522
|
+
│ ├── configs.ts
|
|
523
|
+
│ └── index.ts
|
|
356
524
|
├── next/
|
|
357
525
|
│ ├── configs.ts
|
|
358
526
|
│ └── index.ts
|
|
@@ -365,6 +533,7 @@ src/
|
|
|
365
533
|
| Módulo | Propósito |
|
|
366
534
|
| --- | --- |
|
|
367
535
|
| `@skapxd/eslint-opinionated/shared` | Reglas y presets comunes para backend, frontend y paquetes npm. |
|
|
536
|
+
| `@skapxd/eslint-opinionated/nest` | Presets específicos para NestJS. |
|
|
368
537
|
| `@skapxd/eslint-opinionated/next` | Presets específicos para Next.js. |
|
|
369
538
|
| `@skapxd/eslint-opinionated/astro` | Presets específicos para Astro. |
|
|
370
539
|
| `@skapxd/eslint-opinionated` | Entry point principal con todas las reglas y configs. |
|
|
@@ -478,6 +647,68 @@ export default [
|
|
|
478
647
|
];
|
|
479
648
|
```
|
|
480
649
|
|
|
650
|
+
### NestJS
|
|
651
|
+
|
|
652
|
+
```js
|
|
653
|
+
import skapxd from "@skapxd/eslint-opinionated";
|
|
654
|
+
|
|
655
|
+
export default [
|
|
656
|
+
...skapxd.configs.nest,
|
|
657
|
+
];
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Nest trae un modelo de errores por excepciones (`HttpException` + exception
|
|
661
|
+
filters). El preset no pelea contra eso: asigna a cada capa su rol en el
|
|
662
|
+
pipeline de Result:
|
|
663
|
+
|
|
664
|
+
| Capa Nest | Rol | Contrato |
|
|
665
|
+
| --- | --- | --- |
|
|
666
|
+
| Services / use-cases | El dominio puro | Todo retorna `Promise<Result<T, DomainError>>`; `trySafe` en la frontera con Mongoose/Prisma/HTTP |
|
|
667
|
+
| Controllers | La frontera | Consumen el Result con `match()`: rama ok → DTO, rama err → `throw new HttpException(...)`. El `throw` aquí es el idioma del framework, no una fuga |
|
|
668
|
+
| Exception filter global | **El suelo del sistema** | Recibe todo lo que escapó, con el `cause` completo → telemetría/log (ver "El suelo del sistema") |
|
|
669
|
+
|
|
670
|
+
Detalles del preset:
|
|
671
|
+
|
|
672
|
+
- Aplica a `src/**/*.ts` — `dev/`, `scripts/`, `e2e/` e `integration-test/`
|
|
673
|
+
quedan fuera a propósito: no son la app.
|
|
674
|
+
- Los entrypoints (`main.ts`, `instrumentation.ts`, `app-cluster.ts`) están
|
|
675
|
+
exentos de `await-requires-result`: el bootstrap debe crashear ruidoso.
|
|
676
|
+
Con `no-floating-promises` activa, el clásico `bootstrap();` del `main.ts`
|
|
677
|
+
se escribe `void bootstrap();` — fire-and-forget declarado.
|
|
678
|
+
- Los specs colocados (`*.spec.ts`, `*.e2e-spec.ts`) relajan
|
|
679
|
+
`await-requires-result`, `no-try-catch`, `result-error-requires-handling` y
|
|
680
|
+
`@typescript-eslint/no-non-null-assertion` (el `!` sobre un fixture es el
|
|
681
|
+
arrange del test): un test awaitea helpers libremente y descartar un Result
|
|
682
|
+
en una aserción no es perder un trace. `no-floating-promises` sigue activa
|
|
683
|
+
en specs: un `await` olvidado es un falso verde.
|
|
684
|
+
- Activa `skapxd/nest-no-result-response` (ver su sección): un controller
|
|
685
|
+
jamás retorna el Result crudo.
|
|
686
|
+
- **El contrato Swagger vive en los DTOs, no en el controller.** El preset
|
|
687
|
+
asume el plugin `@nestjs/swagger` activo en `nest-cli.json` (introspecciona
|
|
688
|
+
query/params/body y tipo de retorno solo): `nest-dto-requires-api-property`
|
|
689
|
+
exige `@ApiProperty` en toda propiedad pública de un `*.dto.ts`, y
|
|
690
|
+
`nest-no-swagger-in-controllers` prohíbe los decoradores redundantes
|
|
691
|
+
(`@ApiOperation`, `@ApiResponse`, `@ApiParam`, ...) en los controllers —
|
|
692
|
+
solo se permiten los que el plugin no puede inferir: `ApiExcludeEndpoint`,
|
|
693
|
+
`ApiTags`, `ApiBearerAuth`, `ApiConsumes`/`ApiBody` (uploads multipart).
|
|
694
|
+
- **Los DTOs de input validan en runtime**: `nest-dto-requires-validation`
|
|
695
|
+
exige class-validator en cada propiedad, coherencia `?` ↔ `@IsOptional`, y
|
|
696
|
+
`@Type` de class-transformer junto a `@ValidateNested`. Los DTOs de
|
|
697
|
+
respuesta (`out-*`, `*-response`, ...) quedan exentos.
|
|
698
|
+
- **Una clase = una responsabilidad**: `max-public-methods` (de las reglas
|
|
699
|
+
base) corre con los hooks de Nest inyectados vía `ignore`, y se apaga en
|
|
700
|
+
`*.controller.ts`/`*.gateway.ts` donde el framework dicta la forma.
|
|
701
|
+
`nest-no-direct-instantiation` (dependencias por constructor, no `new`) en
|
|
702
|
+
`*.service.ts`; `nest-no-inline-query-params` en `*.controller.ts` (2+
|
|
703
|
+
query params → DTO consolidado).
|
|
704
|
+
- **La configuración del proyecto también se lintea**: las premisas de las
|
|
705
|
+
que dependen las demás reglas se verifican, no se asumen.
|
|
706
|
+
`nest-requires-swagger-plugin` lee el `nest-cli.json` real (subiendo desde
|
|
707
|
+
`src/main.ts`) y exige el plugin `@nestjs/swagger`;
|
|
708
|
+
`nest-validation-pipe-config` exige `transform: true` (sin él, los `@Type`
|
|
709
|
+
de los DTOs no hacen nada) y `whitelist: true` (sin él, las props sin
|
|
710
|
+
decorador pasan crudas) en todo `new ValidationPipe`.
|
|
711
|
+
|
|
481
712
|
### Astro
|
|
482
713
|
|
|
483
714
|
```js
|
|
@@ -573,14 +804,30 @@ de cada regla):
|
|
|
573
804
|
| `async-functions-return-result` | `allowFilePatterns` (globs), `allowNamePatterns` (regex), `checkMissingReturnType`, `checkMissingReturnTypeWhenCallNames`, `requireCallNames`, `promiseTypeNames`, `resultTypeNames` |
|
|
574
805
|
| `await-requires-result` | `allowFilePatterns` (globs), `trySafeCallNames` |
|
|
575
806
|
| `max-hook-size` | `maxLines`, `maxUseState` |
|
|
807
|
+
| `class-properties-require-readonly` | `allowFilePatterns` (globs), `allowPropertyPatterns` (regex), `ormModuleSources` (default `["@nestjs/mongoose", "typeorm"]`) |
|
|
808
|
+
| `max-public-methods` | `allowFilePatterns` (globs), `max` (default `1`), `ignore` (aditivo a los hooks de Nest) |
|
|
809
|
+
| `no-accessors` | `allowFilePatterns` (globs) |
|
|
810
|
+
| `nest-dto-requires-api-property` | `allowFilePatterns` (globs), `dtoFilePatterns` (default `["*.dto.ts"]`), `apiPropertyDecoratorNames` |
|
|
811
|
+
| `nest-dto-requires-validation` | `allowFilePatterns` (globs), `dtoFilePatterns`, `outputDtoFilePatterns`, `outputDtoClassPatterns` (regex), `optionalDecoratorNames` |
|
|
812
|
+
| `nest-no-direct-instantiation` | `allowFilePatterns` (globs), `internalPatterns` (regex), `allowedPatterns` (regex), `allowedClassPatterns` (regex, default `(Error|Exception|Event)$`) |
|
|
813
|
+
| `nest-no-inline-query-params` | `allowFilePatterns` (globs), `max` (default `1`) |
|
|
814
|
+
| `nest-no-result-response` | `allowFilePatterns` (globs), `controllerDecoratorNames` (default `["Controller"]`) |
|
|
815
|
+
| `nest-no-swagger-in-controllers` | `allowFilePatterns` (globs), `allowedDecoratorNames`, `controllerDecoratorNames` |
|
|
816
|
+
| `nest-requires-swagger-plugin` | `allowFilePatterns` (globs), `mainFilePatterns` (default `["src/main.ts"]`) |
|
|
817
|
+
| `nest-validation-pipe-config` | `allowFilePatterns` (globs), `requiredPipeOptions` (default `["transform", "whitelist"]`) |
|
|
576
818
|
| `no-deep-relative-imports` | `maxDepth` |
|
|
577
819
|
| `no-default-export` | `allowFilePatterns` (globs, aditivos a los integrados) |
|
|
820
|
+
| `no-else` | `allowFilePatterns` (globs) |
|
|
578
821
|
| `no-emoji` | `allowFilePatterns` (globs) |
|
|
822
|
+
| `no-impossible-branch` | las de la regla original de typescript-eslint (`allowConstantLoopConditions`, ...) |
|
|
579
823
|
| `no-functions-inside-components` | `allowJsxCallbacks`, `allowArrayMapCallbacks` (ambas `true` por defecto) |
|
|
580
824
|
| `no-nested-if` | `allowFilePatterns` (globs) |
|
|
581
825
|
| `no-promise-chain` | `methods` |
|
|
826
|
+
| `no-runtime-state-guard` | `allowFilePatterns` (globs) |
|
|
582
827
|
| `no-tunnel-props` | `allowFilePatterns` (globs), `allowPropPatterns` (regex) |
|
|
583
828
|
| `prefer-abort-signal` | `allowFilePatterns` (globs), `effectNames` (default `["useEffect", "useLayoutEffect"]`) |
|
|
829
|
+
| `prefer-tagged-union-state` | `allowFilePatterns` (globs), `loadingPatterns` (regex, en minúsculas), `errorPatterns` (regex, en minúsculas) |
|
|
830
|
+
| `requires-strict-tsconfig` | `allowFilePatterns` (globs), `anchorFilePatterns` (globs), `requiredCompilerOptions` |
|
|
584
831
|
| `result-error-requires-handling` | `allowFilePatterns` (globs) |
|
|
585
832
|
|
|
586
833
|
Los `allowFilePatterns` de todas las reglas son **globs** (`*` un segmento,
|
|
@@ -594,18 +841,34 @@ matchea en cualquier carpeta). Las 7 reglas restantes no tienen opciones: su
|
|
|
594
841
|
| --- | --- |
|
|
595
842
|
| `skapxd/one-root-function-per-file` | Un archivo, una función top-level semántica. |
|
|
596
843
|
| `skapxd/async-functions-return-result` | Funciones async de dominio deben retornar `Promise<Result<...>>`. **Apagada por defecto; opt-in** (ver motivos en su sección). |
|
|
844
|
+
| `skapxd/requires-strict-tsconfig` | El `tsconfig` debe ser implacable (`strict`, `noImplicitReturns`, `noUncheckedIndexedAccess`): sin ellos, el compilador no puede hacer irrepresentable lo inválido. |
|
|
597
845
|
| `skapxd/result-error-requires-cause` | Un `Result.err` derivado debe preservar `cause: result.error`. |
|
|
598
846
|
| `skapxd/result-error-requires-handling` | Prohíbe descartar en silencio un Result fallido: el error se transforma o se entrega, nunca se ignora. |
|
|
599
847
|
| `skapxd/await-requires-result` | Todo `await` debe resolver en un `Result`: o la función llamada retorna `Promise<Result<...>>` (preferido) o se envuelve en `trySafe`. **Obligatoria en todos los presets tipados.** |
|
|
600
848
|
| `skapxd/no-ad-hoc-ok-result` | Evita contratos `{ ok: ... }` hechos a mano en async exports. |
|
|
601
849
|
| `skapxd/max-hook-size` | Marca hooks grandes o con demasiados `useState`. |
|
|
850
|
+
| `skapxd/class-properties-require-readonly` | Toda propiedad de clase es `readonly`: el cambio se modela con instancias nuevas, no con mutación. |
|
|
851
|
+
| `skapxd/max-public-methods` | Una clase, una responsabilidad: máximo N métodos públicos (default 1). Agnóstica al framework, en las reglas base; el preset `nest` le inyecta sus hooks. |
|
|
852
|
+
| `skapxd/no-accessors` | Prohíbe `get`/`set`: un método explícito dice la verdad; el accessor esconde computación (y métodos disfrazados). |
|
|
602
853
|
| `skapxd/jsx-return-name-pascal-case` | Funciones que retornan JSX deben nombrarse como componentes. |
|
|
854
|
+
| `skapxd/nest-dto-requires-api-property` | Toda propiedad pública de un `*.dto.ts` lleva `@ApiProperty`: el contrato HTTP se documenta en el DTO. Preset `nest`. |
|
|
855
|
+
| `skapxd/nest-dto-requires-validation` | Los DTOs de input validan en runtime: class-validator en cada propiedad, `@IsOptional` si hay `?`, `@Type` junto a `@ValidateNested`. Preset `nest`. |
|
|
856
|
+
| `skapxd/nest-no-direct-instantiation` | Prohíbe `new` sobre imports internos en services: las dependencias entran por el constructor (DI). Preset `nest`. |
|
|
857
|
+
| `skapxd/nest-no-inline-query-params` | Dos o más `@Query('x')`/`@ApiQuery` individuales son un DTO disfrazado: consolida en `@Query() filters: Dto`. Preset `nest`. |
|
|
858
|
+
| `skapxd/nest-no-result-response` | Los métodos de un `@Controller` no retornan `Result`: el envelope se serializaría al cliente. La activa el preset `nest`. |
|
|
859
|
+
| `skapxd/nest-no-swagger-in-controllers` | Los controllers no se llenan de decoradores de swagger; el plugin introspecciona los DTOs. Preset `nest`. |
|
|
860
|
+
| `skapxd/nest-requires-swagger-plugin` | `nest-cli.json` debe tener el plugin `@nestjs/swagger`: la premisa de las reglas de swagger, verificada. Preset `nest`. |
|
|
861
|
+
| `skapxd/nest-validation-pipe-config` | Todo `new ValidationPipe` configura `transform` y `whitelist`: la premisa de las reglas de DTOs. Preset `nest`. |
|
|
603
862
|
| `skapxd/no-deep-relative-imports` | Limita la profundidad de los imports relativos (`../`). |
|
|
604
863
|
| `skapxd/no-default-export` | Prohíbe `export default`; el nombre del símbolo es el contrato. Exime configs/stories y, en el preset `next`, los entrypoints del App Router. |
|
|
864
|
+
| `skapxd/no-else` | Prohíbe `else`/`else if`: el else es el estado sin nombre. Retorno anticipado, ternario simple o `match()`. |
|
|
605
865
|
| `skapxd/no-emoji` | Prohíbe emojis en strings y JSX; cada sistema los renderiza distinto. Usa un icono SVG. |
|
|
866
|
+
| `skapxd/no-impossible-branch` | Condiciones que el type-checker demuestra constantes: la pregunta ya tiene respuesta. Es `@typescript-eslint/no-unnecessary-condition` con nombre semántico y mensajes que enseñan el fix. |
|
|
606
867
|
| `skapxd/no-nested-if` | Prohíbe `if` anidados: retorno anticipado o `match()`. Menos carga cognitiva y sin puntos ciegos para las demás reglas. |
|
|
868
|
+
| `skapxd/no-runtime-state-guard` | Prohíbe `if (this.x) throw` en métodos: el estado inválido se hace irrepresentable en el tipo, no se vigila en runtime. |
|
|
607
869
|
| `skapxd/no-tunnel-props` | Ninguna prop viaja más de un nivel: quien la recibe no puede reenviarla a otro componente. Mata el prop drilling. |
|
|
608
870
|
| `skapxd/prefer-abort-signal` | Listeners en efectos se limpian con `AbortController` (`{ signal }` + `abort()`), no con `removeEventListener`. |
|
|
871
|
+
| `skapxd/prefer-tagged-union-state` | Prohíbe estados inconsistentes representables: flag de loading + campo de error independientes → unión etiquetada. |
|
|
609
872
|
| `skapxd/no-functions-inside-components` | Prohíbe definir funciones dentro de componentes React. |
|
|
610
873
|
| `skapxd/no-try-catch` | Prohíbe `try/catch`; usa `trySafe` de `@skapxd/result`. |
|
|
611
874
|
| `skapxd/no-promise-chain` | Prohíbe `.then/.catch/.finally`; usa `await` (+ `trySafe`). |
|
|
@@ -708,6 +971,84 @@ Todas las opciones, con sus defaults:
|
|
|
708
971
|
}]
|
|
709
972
|
```
|
|
710
973
|
|
|
974
|
+
### `skapxd/requires-strict-tsconfig`
|
|
975
|
+
|
|
976
|
+
Todo el sistema descansa en que el compilador pueda hacer irrepresentables
|
|
977
|
+
los estados inválidos — y eso exige un `tsconfig` implacable. Esta regla lee
|
|
978
|
+
el `tsconfig.json` **real** del proyecto (con la API de TypeScript: soporta
|
|
979
|
+
JSONC y resuelve la cadena de `extends`) y exige los flags, reportando una
|
|
980
|
+
vez por proyecto: si existe un archivo ancla (`anchorFilePatterns`, default
|
|
981
|
+
`src/main.ts(x)`/`src/index.ts(x)`), el reporte le pertenece a ese archivo;
|
|
982
|
+
si el proyecto no tiene entrypoint clásico (Astro, librerías), reporta sobre
|
|
983
|
+
el primer archivo del run y los demás callan — un proyecto sin ancla no se
|
|
984
|
+
queda sin guardián:
|
|
985
|
+
|
|
986
|
+
- `strict` — sin él, el sistema de tipos está apagado a medias.
|
|
987
|
+
- `noImplicitReturns` — una rama que sale sin valor deja de ser silenciosa
|
|
988
|
+
(la pareja en compilación de `no-else`).
|
|
989
|
+
- `noUncheckedIndexedAccess` — `array[i]` y los accesos dinámicos confiesan
|
|
990
|
+
su `undefined` en vez de fingir.
|
|
991
|
+
|
|
992
|
+
`strict: true` **no implica** los otros dos: hay que pedirlos explícitos.
|
|
993
|
+
Fuera del default, a propósito: `exactOptionalPropertyTypes` y
|
|
994
|
+
`strictPropertyInitialization` chocan con los DTOs de class-transformer y
|
|
995
|
+
con muchas librerías — se agregan vía `requiredCompilerOptions` si el
|
|
996
|
+
proyecto los soporta.
|
|
997
|
+
|
|
998
|
+
Además, los **presets tipados activan reglas curadas de typescript-eslint**
|
|
999
|
+
(que ya es peer dependency — no se reimplementan):
|
|
1000
|
+
|
|
1001
|
+
- `@typescript-eslint/no-explicit-any` — `any` apaga el sistema de tipos:
|
|
1002
|
+
todo el esfuerzo muere donde aparece uno.
|
|
1003
|
+
- `@typescript-eslint/consistent-type-definitions` con `type` — las uniones
|
|
1004
|
+
discriminadas son types.
|
|
1005
|
+
- `@typescript-eslint/no-floating-promises` — cierra el hueco que
|
|
1006
|
+
`await-requires-result` no ve: una llamada async **sin** `await` no produce
|
|
1007
|
+
`AwaitExpression`, así que el rechazo muere sin pasar por `trySafe`
|
|
1008
|
+
(medido: 12 promesas flotantes vivas en un backend Nest real). La única
|
|
1009
|
+
salida es `void promesa()`: fire-and-forget declarado y greppeable.
|
|
1010
|
+
- `@typescript-eslint/no-non-null-assertion` — `!` es "cállate, yo sé más
|
|
1011
|
+
que tú" dicho al compilador. Si no puede ser nulo, que lo diga el tipo.
|
|
1012
|
+
(En `nest/tests` queda apagada: el `!` sobre un fixture cuya existencia el
|
|
1013
|
+
propio test garantiza es el arrange, no una mentira.)
|
|
1014
|
+
- `skapxd/no-impossible-branch` — la generalización type-aware de
|
|
1015
|
+
`no-runtime-state-guard`: si el tipo dice que un estado es imposible, el
|
|
1016
|
+
guard defensivo sobra; si el guard hace falta, lo que está mal es el tipo.
|
|
1017
|
+
Es `@typescript-eslint/no-unnecessary-condition` **re-registrada bajo
|
|
1018
|
+
nuestro namespace** (ver su sección): mismo motor, nombre que dice lo que
|
|
1019
|
+
defiende y mensajes que enseñan el fix. Por eso esta regla y
|
|
1020
|
+
`requires-strict-tsconfig` van juntas: sin `noUncheckedIndexedAccess`,
|
|
1021
|
+
`array[i]` miente y la regla acusaría guards necesarios.
|
|
1022
|
+
- `@typescript-eslint/ban-ts-comment` — un error de tipos se arregla
|
|
1023
|
+
modelando mejor, no silenciando la alarma: `@ts-ignore` y `@ts-nocheck`
|
|
1024
|
+
prohibidos. `@ts-expect-error` **con descripción** queda permitido: es la
|
|
1025
|
+
forma legítima de testear que un estado inválido de verdad no compila.
|
|
1026
|
+
|
|
1027
|
+
Ausencias deliberadas, no olvidos:
|
|
1028
|
+
|
|
1029
|
+
| Regla ausente | Por qué |
|
|
1030
|
+
| --- | --- |
|
|
1031
|
+
| `switch-exhaustiveness-check` | `prefer-ts-pattern` prohíbe el `switch` entero; `match().exhaustive()` da la misma garantía sin él. |
|
|
1032
|
+
| `prefer-readonly` | Superada por `class-properties-require-readonly`: exige `readonly` en la declaración, no solo en privados nunca reasignados. |
|
|
1033
|
+
| `strict-boolean-expressions` | Castiga narrowing legítimo por cientos (560 hallazgos en un backend real) sin hacer irrepresentable ningún estado nuevo. Ruido, no señal. |
|
|
1034
|
+
| `explicit-module-boundary-types` | Los contratos que importan ya están gobernados (`await-requires-result`, `nest-no-result-response`); anotar el resto es ceremonia (198 hallazgos) que la inferencia resuelve sin perder garantías. |
|
|
1035
|
+
| `prefer-readonly-parameter-types` | Impracticable con cualquier parámetro que venga de una librería externa. |
|
|
1036
|
+
|
|
1037
|
+
Complemento recomendado (fuera del alcance de un linter): **tests a nivel de
|
|
1038
|
+
tipos**. Si el dominio vive en los tipos, los tipos también se testean — con
|
|
1039
|
+
[`expectTypeOf` de vitest](https://vitest.dev/guide/testing-types) (ya está en
|
|
1040
|
+
tu stack, sin instalar `tsd`) o con `@ts-expect-error` descrito, que es
|
|
1041
|
+
exactamente el caso que `ban-ts-comment` deja abierto:
|
|
1042
|
+
|
|
1043
|
+
```ts
|
|
1044
|
+
import { expectTypeOf } from "vitest";
|
|
1045
|
+
|
|
1046
|
+
test("un pedido cancelado no puede tener trackingId", () => {
|
|
1047
|
+
expectTypeOf<Extract<Order, { status: "cancelled" }>>()
|
|
1048
|
+
.not.toHaveProperty("trackingId");
|
|
1049
|
+
});
|
|
1050
|
+
```
|
|
1051
|
+
|
|
711
1052
|
### `skapxd/result-error-requires-cause`
|
|
712
1053
|
|
|
713
1054
|
Evita perder el error original al transformar un `Result` fallido:
|
|
@@ -885,6 +1226,77 @@ Sirve para código de pegamento, pero deja el error sin modelar (`Result<T,
|
|
|
885
1226
|
unknown>`). Cuando la misma operación se repite o el error importa, el mensaje
|
|
886
1227
|
de la regla empuja hacia el camino 1.
|
|
887
1228
|
|
|
1229
|
+
### `skapxd/nest-no-swagger-in-controllers`
|
|
1230
|
+
|
|
1231
|
+
La contracara de la anterior: con el plugin de `@nestjs/swagger` activo en
|
|
1232
|
+
`nest-cli.json`, los decoradores de documentación en el controller son ruido
|
|
1233
|
+
redundante — el plugin ya introspecciona los DTOs de input y el tipo de
|
|
1234
|
+
retorno. Un controller lleno de `@ApiOperation`/`@ApiResponse`/`@ApiParam`
|
|
1235
|
+
entierra la lógica de la frontera bajo metadatos que viven mejor en el DTO:
|
|
1236
|
+
|
|
1237
|
+
```ts
|
|
1238
|
+
@Controller("users")
|
|
1239
|
+
export class UsersController {
|
|
1240
|
+
@ApiOperation({ summary: "Busca un usuario" }) // ❌ redundante con el plugin
|
|
1241
|
+
@ApiResponse({ status: 200, type: UserDto }) // ❌ el tipo de retorno ya lo dice
|
|
1242
|
+
@ApiParam({ name: "id" }) // ❌ el DTO de params ya lo dice
|
|
1243
|
+
@Get(":id")
|
|
1244
|
+
findOne(@Param() params: FindUserParamsDto): Promise<UserDto> { ... }
|
|
1245
|
+
}
|
|
1246
|
+
```
|
|
1247
|
+
|
|
1248
|
+
Solo se permiten los decoradores que el plugin **no puede inferir**
|
|
1249
|
+
(`allowedDecoratorNames`, configurable): `ApiExcludeEndpoint` (ocultar rutas
|
|
1250
|
+
internas), `ApiTags` (agrupación), `ApiBearerAuth` (auth), y
|
|
1251
|
+
`ApiConsumes`/`ApiBody` (uploads multipart, que la introspección no ve).
|
|
1252
|
+
|
|
1253
|
+
La detección compara contra los **imports reales de `@nestjs/swagger`** del
|
|
1254
|
+
archivo: un decorador propio que se llame `ApiOperation` no se toca. Solo
|
|
1255
|
+
aplica dentro de clases `@Controller`.
|
|
1256
|
+
|
|
1257
|
+
### `skapxd/nest-requires-swagger-plugin`
|
|
1258
|
+
|
|
1259
|
+
Las reglas de swagger del preset (`nest-no-swagger-in-controllers`,
|
|
1260
|
+
`nest-dto-requires-api-property`) descansan sobre una premisa: el plugin
|
|
1261
|
+
`@nestjs/swagger` activo en `nest-cli.json`, que introspecciona DTOs y tipos
|
|
1262
|
+
de retorno. Esta regla **verifica la premisa en vez de asumirla**: anclada al
|
|
1263
|
+
entrypoint (`mainFilePatterns`, default `src/main.ts`, un reporte por
|
|
1264
|
+
proyecto), sube por las carpetas hasta el `nest-cli.json` real y exige:
|
|
1265
|
+
|
|
1266
|
+
```jsonc
|
|
1267
|
+
// nest-cli.json
|
|
1268
|
+
{
|
|
1269
|
+
"compilerOptions": {
|
|
1270
|
+
"plugins": ["@nestjs/swagger"] // ✅ (también acepta { "name": "..." })
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
```
|
|
1274
|
+
|
|
1275
|
+
Sin el plugin, el swagger queda vacío — y como el preset prohíbe documentarlo
|
|
1276
|
+
a mano en los controllers, el error te lo dice en el primer lint, no en el
|
|
1277
|
+
primer deploy.
|
|
1278
|
+
|
|
1279
|
+
### `skapxd/nest-validation-pipe-config`
|
|
1280
|
+
|
|
1281
|
+
La otra premisa verificada: todo `new ValidationPipe(...)` (el real, importado
|
|
1282
|
+
de `@nestjs/common`) debe configurar las dos opciones que hacen reales los
|
|
1283
|
+
contratos de los DTOs:
|
|
1284
|
+
|
|
1285
|
+
```ts
|
|
1286
|
+
app.useGlobalPipes(
|
|
1287
|
+
new ValidationPipe({
|
|
1288
|
+
transform: true, // sin él, class-transformer no corre: los @Type no hacen NADA
|
|
1289
|
+
whitelist: true, // sin él, las props sin decorador pasan crudas al dominio
|
|
1290
|
+
// ...el resto (exceptionFactory, transformOptions) es tuyo
|
|
1291
|
+
}),
|
|
1292
|
+
);
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
`new ValidationPipe()` sin opciones, con una faltante o con `transform: false`
|
|
1296
|
+
se reporta. Si las opciones llegan como variable, se resuelve por scope; un
|
|
1297
|
+
identifier irresoluble o un spread reciben el beneficio de la duda.
|
|
1298
|
+
`requiredPipeOptions` es configurable (p. ej. añadir `forbidNonWhitelisted`).
|
|
1299
|
+
|
|
888
1300
|
### `skapxd/no-ad-hoc-ok-result`
|
|
889
1301
|
|
|
890
1302
|
Prohíbe que una función async **exportada** retorne objetos literales con la
|
|
@@ -945,6 +1357,287 @@ de componentes detectan "componente" por nombre PascalCase, así que una
|
|
|
945
1357
|
función `renderX` que devuelve JSX escaparía de ellas. Esta la captura y
|
|
946
1358
|
fuerza el rename — y con el nombre corregido, las demás ya la ven.
|
|
947
1359
|
|
|
1360
|
+
### `skapxd/no-accessors`
|
|
1361
|
+
|
|
1362
|
+
Prohíbe `get`/`set` en clases y objetos literales. Un accessor es un método
|
|
1363
|
+
con sintaxis de propiedad: esconde computación tras un acceso que parece
|
|
1364
|
+
inocente (`config.token` que en realidad ejecuta código), y abre la puerta al
|
|
1365
|
+
**método disfrazado** — un `get sendMessage() { return (...) => ... }` que
|
|
1366
|
+
escapaba de `max-public-methods`:
|
|
1367
|
+
|
|
1368
|
+
```ts
|
|
1369
|
+
class Connection {
|
|
1370
|
+
get socket() { return this.current; } // ❌ computación disfrazada de propiedad
|
|
1371
|
+
socket() { return this.current; } // ✅ el call site dice la verdad: socket()
|
|
1372
|
+
}
|
|
1373
|
+
```
|
|
1374
|
+
|
|
1375
|
+
Si algo es un dato, es una propiedad `readonly`; si algo es comportamiento,
|
|
1376
|
+
es un método explícito que cuenta en la superficie pública. No hay tercera
|
|
1377
|
+
categoría.
|
|
1378
|
+
|
|
1379
|
+
### `skapxd/class-properties-require-readonly`
|
|
1380
|
+
|
|
1381
|
+
Toda propiedad de clase (incluidas las parameter properties del constructor)
|
|
1382
|
+
lleva `readonly`. El estado mutable es la raíz de los **estados
|
|
1383
|
+
inconsistentes** — la misma enfermedad del `useState` con `isLoading`,
|
|
1384
|
+
`error` y `value` llenos a la vez que motivó este paquete: si los campos
|
|
1385
|
+
pueden mutar por separado, las combinaciones imposibles se vuelven posibles.
|
|
1386
|
+
El cambio se modela creando instancias nuevas:
|
|
1387
|
+
|
|
1388
|
+
```ts
|
|
1389
|
+
class Loan {
|
|
1390
|
+
constructor(
|
|
1391
|
+
readonly amount: number, // ✅
|
|
1392
|
+
private readonly term: number, // ✅ parameter property también
|
|
1393
|
+
) {}
|
|
1394
|
+
|
|
1395
|
+
withAmount(amount: number): Loan {
|
|
1396
|
+
return new Loan(amount, this.term); // el "cambio": una instancia nueva
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
class Cache {
|
|
1401
|
+
private entries: string[] = []; // ❌ private no exime: mutable es mutable
|
|
1402
|
+
}
|
|
1403
|
+
```
|
|
1404
|
+
|
|
1405
|
+
La mutación inherente (la conexión de un socket que se reemplaza al
|
|
1406
|
+
reconectar) **se declara visible** en `allowPropertyPatterns: ["^currentSocket$"]`
|
|
1407
|
+
— una decisión en la config, greppeable, no un default silencioso.
|
|
1408
|
+
|
|
1409
|
+
**Compatibilidad con NestJS, investigada y verificada:**
|
|
1410
|
+
|
|
1411
|
+
- **DTOs ✅ sin fricción** (verificado empíricamente con class-transformer +
|
|
1412
|
+
class-validator reales): `readonly` es chequeo de compilación que se borra
|
|
1413
|
+
en runtime — `plainToInstance` asigna, `@Type` convierte, los anidados se
|
|
1414
|
+
instancian y la validación corre igual. El issue conocido de
|
|
1415
|
+
class-transformer ([typestack/class-transformer#250](https://github.com/typestack/class-transformer/issues/250))
|
|
1416
|
+
es sobre `private readonly` detrás de *getters* (accessors) — patrón que
|
|
1417
|
+
`no-accessors` ya prohíbe.
|
|
1418
|
+
- **Capa de persistencia ⚠️ exención POR PROPIEDAD, no por archivo**: una
|
|
1419
|
+
propiedad decorada por el ORM (`@Prop` de `@nestjs/mongoose`, `@Column` y
|
|
1420
|
+
compañía de `typeorm` — verificados contra los imports reales,
|
|
1421
|
+
`ormModuleSources` configurable) le pertenece al ORM y a su modelo de
|
|
1422
|
+
mutación (`doc.campo = x; await doc.save()` no compila contra readonly).
|
|
1423
|
+
La precisión importa: una propiedad **sin** `@Prop` dentro de un
|
|
1424
|
+
`*.schema.ts` es estado de clase normal (campos virtuales, caches) y sí
|
|
1425
|
+
exige `readonly` — la exención por nombre de archivo la habría silenciado.
|
|
1426
|
+
- **Cuidado con los TIPOS array readonly** (`tags: readonly string[]`,
|
|
1427
|
+
`ReadonlyArray<T>`): el plugin de `@nestjs/swagger` degrada su inferencia
|
|
1428
|
+
con ellos ([nestjs/swagger#2413](https://github.com/nestjs/swagger/issues/2413)).
|
|
1429
|
+
Esta regla exige el modificador en la *propiedad* (`readonly tags: string[]`),
|
|
1430
|
+
que es inocuo para el plugin — no uses los tipos array readonly en DTOs.
|
|
1431
|
+
|
|
1432
|
+
### `skapxd/max-public-methods`
|
|
1433
|
+
|
|
1434
|
+
El `one-root-function-per-file` del mundo de clases: **una clase, una
|
|
1435
|
+
responsabilidad** — máximo `max` métodos públicos (default `1`). Es la regla
|
|
1436
|
+
que convierte un `loans.service.ts` de 1965 líneas en una carpeta de casos de
|
|
1437
|
+
uso (`find-apc-score.service.ts`, `create-signature.service.ts`, ...).
|
|
1438
|
+
|
|
1439
|
+
Es **agnóstica al framework** y vive en las reglas base: una clase en Nest,
|
|
1440
|
+
Astro, Next o un proyecto Vite responde al mismo contrato. El conocimiento
|
|
1441
|
+
del framework lo inyecta cada preset vía `ignore` — la regla en sí no sabe
|
|
1442
|
+
qué es NestJS.
|
|
1443
|
+
|
|
1444
|
+
```ts
|
|
1445
|
+
// ❌ dos casos de uso conviviendo
|
|
1446
|
+
export class ApcService {
|
|
1447
|
+
async getScore(id: string) { ... }
|
|
1448
|
+
async refreshScore(id: string) { ... }
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// ✅ un caso de uso con su séquito privado
|
|
1452
|
+
export class FindApcScoreService {
|
|
1453
|
+
constructor(private readonly repository: ApcRepository) {}
|
|
1454
|
+
async execute(id: string) { return this.normalize(...); }
|
|
1455
|
+
private normalize(raw: unknown) { ... }
|
|
1456
|
+
}
|
|
1457
|
+
```
|
|
1458
|
+
|
|
1459
|
+
No cuentan: constructor, getters/setters, `private`/`protected`, `#privados`
|
|
1460
|
+
y el prefijo `_`. `ignore` exime nombres por opción — así el **preset `nest`**
|
|
1461
|
+
inyecta sus hooks (`onModuleInit`, `onApplicationBootstrap`, `canActivate`,
|
|
1462
|
+
`intercept`, `transform`, `catch`, `use`, ...): callbacks que el framework
|
|
1463
|
+
llama, no superficie pública. Fuera de Nest esos nombres no significan nada y
|
|
1464
|
+
cuentan como cualquier método.
|
|
1465
|
+
|
|
1466
|
+
El preset `nest` además la **apaga en `*.controller.ts` y `*.gateway.ts`**:
|
|
1467
|
+
ahí la forma la dicta el framework (un método por ruta/evento) y el límite no
|
|
1468
|
+
aporta semántica. El mensaje de error es un playbook de refactor completo
|
|
1469
|
+
(nombres semánticos, extracción de estado compartido, actualización del
|
|
1470
|
+
módulo y los imports) pensado para que un agente lo ejecute solo.
|
|
1471
|
+
|
|
1472
|
+
### `skapxd/nest-dto-requires-api-property`
|
|
1473
|
+
|
|
1474
|
+
El contrato HTTP — query, params, body y respuesta — se documenta en el DTO,
|
|
1475
|
+
no en el controller. Toda propiedad **pública de instancia** de una clase en
|
|
1476
|
+
un `*.dto.ts` debe llevar `@ApiProperty` o `@ApiPropertyOptional`:
|
|
1477
|
+
|
|
1478
|
+
```ts
|
|
1479
|
+
// create-user.dto.ts
|
|
1480
|
+
export class CreateUserDto {
|
|
1481
|
+
@ApiProperty({ description: "Nombre legal completo", example: "Ana Pérez" })
|
|
1482
|
+
name: string; // ✅
|
|
1483
|
+
|
|
1484
|
+
email: string; // ❌ sin documentar
|
|
1485
|
+
|
|
1486
|
+
@IsString()
|
|
1487
|
+
phone: string; // ❌ class-validator no documenta
|
|
1488
|
+
}
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
El plugin de `@nestjs/swagger` infiere el **tipo**, pero la `description` y el
|
|
1492
|
+
`example` son intención tuya — y son lo que convierte el swagger en un
|
|
1493
|
+
contrato legible (y en un buen cliente generado). Las propiedades `private`,
|
|
1494
|
+
`protected`, `#privadas` y `static` no se exigen: swagger no las serializa.
|
|
1495
|
+
`dtoFilePatterns` ajusta la convención de archivos si no usas `*.dto.ts`.
|
|
1496
|
+
|
|
1497
|
+
### `skapxd/nest-dto-requires-validation`
|
|
1498
|
+
|
|
1499
|
+
El tipo de TypeScript desaparece en runtime: un DTO de input sin
|
|
1500
|
+
class-validator es un contrato de mentira — el `ValidationPipe` deja pasar
|
|
1501
|
+
cualquier cosa (o la descarta en silencio con `whitelist`). Tres contratos en
|
|
1502
|
+
una regla:
|
|
1503
|
+
|
|
1504
|
+
```ts
|
|
1505
|
+
export class CreateLoanDto {
|
|
1506
|
+
@ApiProperty()
|
|
1507
|
+
@IsNumber() // 1. ✅ toda propiedad valida en runtime
|
|
1508
|
+
@IsNotEmpty()
|
|
1509
|
+
amount: number;
|
|
1510
|
+
|
|
1511
|
+
@ApiPropertyOptional()
|
|
1512
|
+
@IsOptional() // 2. ✅ el `?` del tipo y el runtime coinciden
|
|
1513
|
+
@IsNumber()
|
|
1514
|
+
termMonths?: number;
|
|
1515
|
+
|
|
1516
|
+
@ApiProperty()
|
|
1517
|
+
@ValidateNested()
|
|
1518
|
+
@Type(() => AddressDto) // 3. ✅ sin @Type, la validación anidada NO corre
|
|
1519
|
+
address: AddressDto;
|
|
1520
|
+
}
|
|
1521
|
+
```
|
|
1522
|
+
|
|
1523
|
+
1. **Toda propiedad pública** lleva al menos un decorador de class-validator.
|
|
1524
|
+
2. **`?` exige `@IsOptional`** (o `@ValidateIf`): si el tipo dice opcional y el
|
|
1525
|
+
runtime la exige, el contrato miente.
|
|
1526
|
+
3. **`@ValidateNested` exige `@Type(() => Clase)`** de class-transformer: sin
|
|
1527
|
+
él, el objeto anidado llega como plain object y la validación anidada no
|
|
1528
|
+
corre — el bug silencioso clásico (esta regla lo encontró en producción).
|
|
1529
|
+
|
|
1530
|
+
Los **DTOs de respuesta quedan exentos** por dos vías: nombre de archivo
|
|
1531
|
+
(`outputDtoFilePatterns`: `out-*`, `output-*`, `*-response`, `*-result`,
|
|
1532
|
+
`*-output`) y **nombre de clase** (`outputDtoClassPatterns`, regex, default
|
|
1533
|
+
`(Response|Result|Output)(Dto)?$`) — porque un `UploadDocumentResponseDto`
|
|
1534
|
+
puede vivir en un archivo de nombre neutro (`upload-document.dto.ts`) o
|
|
1535
|
+
compartir archivo con DTOs de input, y la exención de la clase no contagia a
|
|
1536
|
+
sus vecinas. El server los produce, no los recibe. La detección compara
|
|
1537
|
+
contra los imports reales de `class-validator`/`class-transformer`, así que
|
|
1538
|
+
un decorador casero homónimo no engaña a la regla.
|
|
1539
|
+
|
|
1540
|
+
**El caso Multer** queda cubierto por el conjunto: el archivo llega como
|
|
1541
|
+
parámetro (`@UploadedFiles() files: Express.Multer.File[]`), nunca en un DTO
|
|
1542
|
+
validable; el schema multipart se documenta inline en el controller con
|
|
1543
|
+
`@ApiConsumes` + `@ApiBody` (permitidos por `nest-no-swagger-in-controllers`:
|
|
1544
|
+
la introspección no ve multipart); y el DTO de respuesta del upload queda
|
|
1545
|
+
exento por nombre de clase. La validación del archivo en sí (tamaño, mimetype)
|
|
1546
|
+
va donde Nest la diseñó: `ParseFilePipe` en el parámetro, no class-validator.
|
|
1547
|
+
|
|
1548
|
+
### `skapxd/nest-no-direct-instantiation`
|
|
1549
|
+
|
|
1550
|
+
En un service, `new FooService()` sobre un import **interno del proyecto**
|
|
1551
|
+
esquiva el contenedor de DI: NestJS no resuelve sus dependencias, no
|
|
1552
|
+
participa del lifecycle, y la clase deja de ser testeable con mocks. Las
|
|
1553
|
+
dependencias entran por el constructor:
|
|
1554
|
+
|
|
1555
|
+
```ts
|
|
1556
|
+
import { FooService } from "#/modules/foo/foo.service";
|
|
1557
|
+
|
|
1558
|
+
const foo = new FooService(); // ❌ esquiva la DI
|
|
1559
|
+
|
|
1560
|
+
constructor(private readonly fooService: FooService) {} // ✅ NestJS resuelve
|
|
1561
|
+
```
|
|
1562
|
+
|
|
1563
|
+
La robustez viene en capas:
|
|
1564
|
+
|
|
1565
|
+
1. **Los globals del runtime nunca se marcan** (`new Date()`, `new Map()`,
|
|
1566
|
+
`new AbortController()`): la regla parte de los **imports internos**
|
|
1567
|
+
(`internalPatterns`: alias `#/`, `@/` y relativos), y un global no se
|
|
1568
|
+
importa. Las librerías externas (`new Logger(...)`) también libres, y los
|
|
1569
|
+
`import type` no cuentan.
|
|
1570
|
+
2. **Exención por nombre de clase** (`allowedClassPatterns`, default
|
|
1571
|
+
`(Error|Exception|Event)$`): errores, excepciones y eventos de dominio se
|
|
1572
|
+
construyen, no se inyectan — vivan en el archivo que vivan.
|
|
1573
|
+
3. **La capa type-aware** (con `projectService`, que el preset trae): la
|
|
1574
|
+
regla resuelve el símbolo de la clase importada y pregunta por el
|
|
1575
|
+
decorador `@Injectable`. Sin el decorador es una clase de valor (un DTO,
|
|
1576
|
+
un mapper puro) y el `new` es legítimo; con él, pertenece al contenedor y
|
|
1577
|
+
se reporta. Irresoluble → conservador, se reporta. En un proyecto real
|
|
1578
|
+
esta capa eliminó el 100% de los falsos positivos restantes.
|
|
1579
|
+
|
|
1580
|
+
`allowedPatterns` (regex de sources) sigue disponible para convenciones
|
|
1581
|
+
propias. El preset la activa en `*.service.ts`.
|
|
1582
|
+
|
|
1583
|
+
### `skapxd/nest-no-inline-query-params`
|
|
1584
|
+
|
|
1585
|
+
Dos o más `@Query('x')` individuales (o `@ApiQuery` sueltos) en un handler
|
|
1586
|
+
son un DTO disfrazado — sin validación automática, sin tipos de verdad y con
|
|
1587
|
+
el controller enterrado en decoradores:
|
|
1588
|
+
|
|
1589
|
+
```ts
|
|
1590
|
+
// ❌ cada query a mano
|
|
1591
|
+
findAll(@Query("status") status?: string, @Query("clientName") name?: string) {}
|
|
1592
|
+
|
|
1593
|
+
// ✅ el DTO consolidado: ValidationPipe valida, swagger documenta, el tipo es real
|
|
1594
|
+
findAll(@Query() filters: ListLoansDto) {}
|
|
1595
|
+
```
|
|
1596
|
+
|
|
1597
|
+
`@Query()` sin argumento (el DTO completo) y un único `@Query('id')` son
|
|
1598
|
+
legítimos (`max` configurable). El mensaje trae el playbook de migración:
|
|
1599
|
+
propiedades `?` + `@IsOptional` + validador + `@ApiPropertyOptional`, y
|
|
1600
|
+
`@Transform`/`@Type` para convertir los strings del query al tipo real.
|
|
1601
|
+
Conecta con `nest-dto-requires-validation`: el DTO que crees ya queda
|
|
1602
|
+
vigilado. Solo el `Query`/`ApiQuery` importados de Nest cuentan.
|
|
1603
|
+
|
|
1604
|
+
### `skapxd/nest-no-result-response`
|
|
1605
|
+
|
|
1606
|
+
El footgun silencioso de mezclar Result con Nest: si un método de un
|
|
1607
|
+
`@Controller` retorna el `Result` crudo, Nest lo serializa tal cual y el
|
|
1608
|
+
cliente recibe `{ ok: false, error: {...} }` con tus internals — tipos de
|
|
1609
|
+
error de dominio, causas, stack traces. Esta regla lo hace imposible:
|
|
1610
|
+
|
|
1611
|
+
```ts
|
|
1612
|
+
@Controller("users")
|
|
1613
|
+
export class UsersController {
|
|
1614
|
+
// ❌ el envelope completo viaja al cliente
|
|
1615
|
+
@Get(":id")
|
|
1616
|
+
async findOne(@Param("id") id: string): Promise<Result<User, UserError>> {
|
|
1617
|
+
return this.usersService.findOne(id);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// ✅ el controller es la frontera: match() traduce
|
|
1621
|
+
@Get(":id")
|
|
1622
|
+
async findOne(@Param("id") id: string): Promise<UserDto> {
|
|
1623
|
+
const user = await this.usersService.findOne(id);
|
|
1624
|
+
|
|
1625
|
+
return match(user)
|
|
1626
|
+
.with({ ok: true }, ({ value }) => toUserDto(value))
|
|
1627
|
+
.with({ ok: false, error: { type: "NOT_FOUND" } }, () => {
|
|
1628
|
+
throw new NotFoundException();
|
|
1629
|
+
})
|
|
1630
|
+
.exhaustive();
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
```
|
|
1634
|
+
|
|
1635
|
+
Es **type-aware**: resuelve el tipo de retorno real del método (anotado o
|
|
1636
|
+
inferido) hasta el `Result` de `@skapxd/result`, así que devolver el Result
|
|
1637
|
+
por indirección tampoco escapa. Solo aplica a clases con `@Controller`
|
|
1638
|
+
(configurable con `controllerDecoratorNames` para decoradores propios); los
|
|
1639
|
+
services retornan Result con orgullo — ese es el dominio.
|
|
1640
|
+
|
|
948
1641
|
### `skapxd/no-deep-relative-imports`
|
|
949
1642
|
|
|
950
1643
|
Limita cuántos niveles puede subir un import relativo. Por defecto **prohíbe
|
|
@@ -1041,6 +1734,37 @@ al default export, basta mapear el named en el import dinámico:
|
|
|
1041
1734
|
const Card = lazy(() => import("./card").then((m) => ({ default: m.Card })));
|
|
1042
1735
|
```
|
|
1043
1736
|
|
|
1737
|
+
### `skapxd/no-else`
|
|
1738
|
+
|
|
1739
|
+
El `if` maneja una condición *nombrada*; el `else` maneja "todo lo demás" —
|
|
1740
|
+
un complemento anónimo cuyo significado el lector deduce negando la
|
|
1741
|
+
condición. Es el último rincón donde un camino vive sin etiqueta, y donde la
|
|
1742
|
+
no-exhaustividad se esconde: una cadena `if/else if/else` sobre flags maneja
|
|
1743
|
+
2 de 4 combinaciones y deja el resto cayendo en un cajón que nadie auditó.
|
|
1744
|
+
|
|
1745
|
+
```ts
|
|
1746
|
+
// ❌ ¿qué ES el else? el lector lo deduce; el compilador no audita nada
|
|
1747
|
+
if (s === "a") { runA(); } else if (s === "b") { runB(); } else { runC(); }
|
|
1748
|
+
|
|
1749
|
+
// ✅ guards: cada salida declara su condición y termina
|
|
1750
|
+
if (!user) return Result.err({ ... });
|
|
1751
|
+
return Result.ok(buildProfile(user));
|
|
1752
|
+
|
|
1753
|
+
// ✅ match: cada variante nombrada y exhaustividad verificada
|
|
1754
|
+
match(state)
|
|
1755
|
+
.with({ status: "a" }, runA)
|
|
1756
|
+
.with({ status: "b" }, runB)
|
|
1757
|
+
.exhaustive();
|
|
1758
|
+
```
|
|
1759
|
+
|
|
1760
|
+
Las salidas: **retorno anticipado** para flujo, **ternario simple** para
|
|
1761
|
+
decisiones de valor (los anidados ya los prohíbe `prefer-ts-pattern`), y
|
|
1762
|
+
**`match().exhaustive()`** para variantes. La única fricción real — dos
|
|
1763
|
+
ramas de efectos en medio de una función — se resuelve extrayendo la función
|
|
1764
|
+
que `one-root-function-per-file` ya pedía. Complementa a `no-nested-if`
|
|
1765
|
+
(profundidad) y a `prefer-tagged-union-state` (este ataca la *declaración*
|
|
1766
|
+
del estado sin nombre; `no-else` ataca su *consumo*).
|
|
1767
|
+
|
|
1044
1768
|
### `skapxd/no-emoji`
|
|
1045
1769
|
|
|
1046
1770
|
Prohíbe emojis en strings, template literals y texto JSX. El problema no es
|
|
@@ -1067,6 +1791,40 @@ eximir archivos completos (fixtures, seeds), usa `allowFilePatterns`:
|
|
|
1067
1791
|
}]
|
|
1068
1792
|
```
|
|
1069
1793
|
|
|
1794
|
+
### `skapxd/no-runtime-state-guard`
|
|
1795
|
+
|
|
1796
|
+
El compañero de `prefer-tagged-union-state` para el comportamiento: cuando un
|
|
1797
|
+
método protege su estado con una comprobación en runtime, la máquina de
|
|
1798
|
+
estados vive en `if` + `throw` — requiere tests para cada ruta inválida y el
|
|
1799
|
+
compilador no puede ayudar (*make invalid states unrepresentable*):
|
|
1800
|
+
|
|
1801
|
+
```ts
|
|
1802
|
+
// ❌ el guard en runtime: probable con tests, invisible para el compilador
|
|
1803
|
+
class Socket {
|
|
1804
|
+
private isConnected = false;
|
|
1805
|
+
emit(event: string) {
|
|
1806
|
+
if (!this.isConnected) throw new Error("Cannot emit: not connected");
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// ✅ cada estado es un tipo: emit NO EXISTE en el socket desconectado
|
|
1811
|
+
class DisconnectedSocket {
|
|
1812
|
+
connect(): ConnectedSocket { ... } // la transición retorna el estado nuevo
|
|
1813
|
+
}
|
|
1814
|
+
class ConnectedSocket {
|
|
1815
|
+
emit(event: string): void { ... } // sin guard: el compilador lo garantiza
|
|
1816
|
+
disconnect(): DisconnectedSocket { ... }
|
|
1817
|
+
}
|
|
1818
|
+
```
|
|
1819
|
+
|
|
1820
|
+
(La variante funcional: la unión discriminada de `prefer-tagged-union-state`,
|
|
1821
|
+
consumida con `match()`.) Solo aplica al **estado propio** (`this.<prop>`) en
|
|
1822
|
+
métodos de clase — validar argumentos o inputs externos es otro territorio
|
|
1823
|
+
(DTOs, `Result`). Un `if` sobre `this` que retorna temprano sin lanzar
|
|
1824
|
+
tampoco se toca. Nota la sinergia con `class-properties-require-readonly`:
|
|
1825
|
+
el flag mutable que este guard necesita ya era ilegal — las dos reglas
|
|
1826
|
+
empujan juntas hacia las transiciones que retornan instancias nuevas.
|
|
1827
|
+
|
|
1070
1828
|
### `skapxd/no-tunnel-props`
|
|
1071
1829
|
|
|
1072
1830
|
**Ninguna prop viaja más de un nivel.** El contrato de saltos: quien **crea**
|
|
@@ -1119,6 +1877,36 @@ en un `<input>` es la frontera con el DOM). Para wrappers legítimos de un
|
|
|
1119
1877
|
design system, exime props por nombre (`allowPropPatterns: ["^className$"]`)
|
|
1120
1878
|
o archivos completos (`allowFilePatterns`).
|
|
1121
1879
|
|
|
1880
|
+
### `skapxd/no-impossible-branch`
|
|
1881
|
+
|
|
1882
|
+
La rama imposible: una condición que el type-checker demuestra constante. Si
|
|
1883
|
+
el tipo dice que un valor siempre es truthy, ese `if` no decide nada; si un
|
|
1884
|
+
`?.` cuelga de algo que nunca es nullish, finge una duda que el modelo ya
|
|
1885
|
+
resolvió. La pregunta ya tiene respuesta — y un `if` que no pregunta es
|
|
1886
|
+
código muerto disfrazado de prudencia.
|
|
1887
|
+
|
|
1888
|
+
```ts
|
|
1889
|
+
const sheet = workbook.Sheets[name]; // tipo: WorkSheet (¿seguro?)
|
|
1890
|
+
if (!sheet) continue; // ❌ "always falsy"... ¿o el tipo miente?
|
|
1891
|
+
```
|
|
1892
|
+
|
|
1893
|
+
El mensaje de error enseña la lección completa: **si la comprobación hace
|
|
1894
|
+
falta en runtime, lo que está mal es el tipo**. El caso clásico es el acceso
|
|
1895
|
+
por índice sin `noUncheckedIndexedAccess` — `array[i]` y `obj[key]` juran que
|
|
1896
|
+
nunca son `undefined`, y esta regla, creyéndoles, acusaría guards necesarios.
|
|
1897
|
+
Por eso va de la mano de `skapxd/requires-strict-tsconfig`, que exige ese
|
|
1898
|
+
flag: primero el tsconfig dice la verdad, después esta regla opina.
|
|
1899
|
+
|
|
1900
|
+
Bajo el capó es `@typescript-eslint/no-unnecessary-condition`
|
|
1901
|
+
([doc original](https://typescript-eslint.io/rules/no-unnecessary-condition/))
|
|
1902
|
+
**re-registrada bajo nuestro namespace**: mismo motor y mismas opciones, pero
|
|
1903
|
+
con un nombre que dice lo que defiende (axioma A1: los estados imposibles son
|
|
1904
|
+
irrepresentables — es la generalización type-aware de
|
|
1905
|
+
`no-runtime-state-guard`) y mensajes en español que explican el fix en vez
|
|
1906
|
+
del críptico "Unnecessary conditional". Los presets tipados activan este
|
|
1907
|
+
nombre y **no** el original: una sola fuente de verdad para configurarla,
|
|
1908
|
+
silenciarla o buscarla.
|
|
1909
|
+
|
|
1122
1910
|
### `skapxd/no-functions-inside-components`
|
|
1123
1911
|
|
|
1124
1912
|
Prohíbe definir funciones **con peso propio** dentro de un componente React
|
|
@@ -1221,6 +2009,80 @@ métodos se prohíben (por defecto los tres):
|
|
|
1221
2009
|
"skapxd/no-promise-chain": ["error", { methods: ["catch"] }]
|
|
1222
2010
|
```
|
|
1223
2011
|
|
|
2012
|
+
### `skapxd/prefer-tagged-union-state`
|
|
2013
|
+
|
|
2014
|
+
La regla temática del paquete: el estado inconsistente que motivó todo esto,
|
|
2015
|
+
ahora prohibido en su origen. Detecta las dos formas de la enfermedad:
|
|
2016
|
+
|
|
2017
|
+
**Forma A — el tipo enfermo**: un flag boolean de "en proceso" conviviendo
|
|
2018
|
+
con un campo de error como propiedades independientes. Las combinaciones
|
|
2019
|
+
imposibles (cargando Y con error, error Y con valor) son *representables*:
|
|
2020
|
+
|
|
2021
|
+
```ts
|
|
2022
|
+
// ❌ 2³ combinaciones; solo 3 tienen sentido
|
|
2023
|
+
type RequestState = { isLoading: boolean; error?: Error; value?: Data };
|
|
2024
|
+
|
|
2025
|
+
// ✅ los estados imposibles no se pueden NI ESCRIBIR
|
|
2026
|
+
type RequestState =
|
|
2027
|
+
| { status: "idle" }
|
|
2028
|
+
| { status: "loading" }
|
|
2029
|
+
| { status: "error"; error: Error }
|
|
2030
|
+
| { status: "ok"; value: Data };
|
|
2031
|
+
```
|
|
2032
|
+
|
|
2033
|
+
La forma A aplica **igual en el back**: la clase de un job con
|
|
2034
|
+
`private isProcessing = false; private lastError?: Error` es la versión OOP
|
|
2035
|
+
de la máquina repartida, y un schema de Mongoose con `@Prop() isSyncing` +
|
|
2036
|
+
`@Prop() syncError` es la versión más grave — **la inconsistencia se
|
|
2037
|
+
persiste en la base de datos**. La regla revisa tipos, interfaces y cuerpos
|
|
2038
|
+
de clase por igual, con verbos de ambos mundos (`loading`, `submitting`,
|
|
2039
|
+
`deploying`, `migrating`, `retrying`, ...).
|
|
2040
|
+
|
|
2041
|
+
**Forma B — la máquina repartida** (front): varios `useState` que en realidad
|
|
2042
|
+
son una sola máquina de estados. Cada transición toca varios setters y los
|
|
2043
|
+
renders intermedios ven combinaciones imposibles:
|
|
2044
|
+
|
|
2045
|
+
```ts
|
|
2046
|
+
// ❌ tres setters para una transición: el render del medio ve mentiras
|
|
2047
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
2048
|
+
const [error, setError] = useState<Error | null>(null);
|
|
2049
|
+
const [user, setUser] = useState<User | null>(null);
|
|
2050
|
+
|
|
2051
|
+
// ✅ UN estado, transición atómica, match() exhaustivo
|
|
2052
|
+
const [state, setState] = useState<RequestState>({ status: "idle" });
|
|
2053
|
+
```
|
|
2054
|
+
|
|
2055
|
+
**Forma C — la transición repartida (evidencia ESTRUCTURAL, sin depender de
|
|
2056
|
+
nombres)**: los setters de `useState` se identifican por *posición en el
|
|
2057
|
+
destructuring* (`const [x, setX] = useState()` — el segundo elemento, se
|
|
2058
|
+
llame como se llame). Si una misma función llama a dos setters distintos,
|
|
2059
|
+
eso **prueba** que esos estados son una sola máquina — entre setter y setter,
|
|
2060
|
+
los renders intermedios ven mentiras:
|
|
2061
|
+
|
|
2062
|
+
```ts
|
|
2063
|
+
const cargar = (respuesta, fallo) => {
|
|
2064
|
+
setDatos(respuesta); // ❌ dos setters en una transición: una máquina
|
|
2065
|
+
setError(fallo); // repartida, aunque `datos` no se llame "loading"
|
|
2066
|
+
};
|
|
2067
|
+
```
|
|
2068
|
+
|
|
2069
|
+
Este detector caza lo que los nombres no ven (estados con nombres exóticos o
|
|
2070
|
+
en español ya cubiertos: `cargando`, `procesando`, `fallo`, ...). El filtro
|
|
2071
|
+
de precisión: al menos uno de los estados co-actualizados debe ser
|
|
2072
|
+
loading/error-ish — resetear dos campos independientes de un formulario no
|
|
2073
|
+
es una máquina.
|
|
2074
|
+
|
|
2075
|
+
Sobre la detección por nombres (formas A y B): es deliberadamente el
|
|
2076
|
+
escalón más bajo de evidencia del paquete — para un tipo *declarado* no hay
|
|
2077
|
+
comportamiento que observar y el nombre es la única señal disponible. El
|
|
2078
|
+
**tipo del campo de error no importa** (`Error`, `string`, código numérico,
|
|
2079
|
+
otro boolean — `isSyncing` + `hasError` es la peor forma): la enfermedad es
|
|
2080
|
+
la coexistencia. Los **callbacks** quedan excluidos (`onError?: (e) => void`,
|
|
2081
|
+
miembros de tipo función): un handler no es estado.
|
|
2082
|
+
`loadingPatterns`/`errorPatterns` ajustan las convenciones. Cierra el círculo con el resto del paquete: la unión etiquetada
|
|
2083
|
+
es a los estados lo que `Result` es a los errores, y `prefer-ts-pattern` te
|
|
2084
|
+
espera con el `match().exhaustive()` al otro lado.
|
|
2085
|
+
|
|
1224
2086
|
### `skapxd/prefer-ts-pattern`
|
|
1225
2087
|
|
|
1226
2088
|
Prohíbe `switch/case` y ternarios anidados, empujando hacia `match()` de
|