@skapxd/eslint-opinionated 0.9.0 → 0.11.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 CHANGED
@@ -548,8 +548,10 @@ de cada regla):
548
548
  | `no-default-export` | `allowFilePatterns` (globs, aditivos a los integrados) |
549
549
  | `no-emoji` | `allowFilePatterns` (globs) |
550
550
  | `no-functions-inside-components` | `allowJsxCallbacks`, `allowArrayMapCallbacks` (ambas `true` por defecto) |
551
+ | `no-nested-if` | `allowFilePatterns` (globs) |
551
552
  | `no-promise-chain` | `methods` |
552
553
  | `no-tunnel-props` | `allowFilePatterns` (globs), `allowPropPatterns` (regex) |
554
+ | `prefer-abort-signal` | `allowFilePatterns` (globs), `effectNames` (default `["useEffect", "useLayoutEffect"]`) |
553
555
 
554
556
  Los `allowFilePatterns` de todas las reglas son **globs** (`*` un segmento,
555
557
  `**` cualquier profundidad, `{a,b}` alternativas; un patrón sin prefijo
@@ -570,7 +572,9 @@ matchea en cualquier carpeta). Las 7 reglas restantes no tienen opciones: su
570
572
  | `skapxd/no-deep-relative-imports` | Limita la profundidad de los imports relativos (`../`). |
571
573
  | `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. |
572
574
  | `skapxd/no-emoji` | Prohíbe emojis en strings y JSX; cada sistema los renderiza distinto. Usa un icono SVG. |
575
+ | `skapxd/no-nested-if` | Prohíbe `if` anidados: retorno anticipado o `match()`. Menos carga cognitiva y sin puntos ciegos para las demás reglas. |
573
576
  | `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. |
577
+ | `skapxd/prefer-abort-signal` | Listeners en efectos se limpian con `AbortController` (`{ signal }` + `abort()`), no con `removeEventListener`. |
574
578
  | `skapxd/no-functions-inside-components` | Prohíbe definir funciones dentro de componentes React. |
575
579
  | `skapxd/no-try-catch` | Prohíbe `try/catch`; usa `trySafe` de `@skapxd/result`. |
576
580
  | `skapxd/no-promise-chain` | Prohíbe `.then/.catch/.finally`; usa `await` (+ `trySafe`). |
@@ -687,10 +691,19 @@ if (!result.ok) {
687
691
  }
688
692
  ```
689
693
 
694
+ Reconoce todas las formas del guard de Result fallido — `!result.ok`,
695
+ `result.ok === false`, `result.ok !== true`, `Result.isErr(result)` y
696
+ `if (result.error)` — y dentro del guard exige el `cause` en todo
697
+ `Result.err(...)`. Un `Result.err()` **sin argumentos** también se reporta:
698
+ descartar el error por completo es el peor caso, no una exención. Y un `cause`
699
+ con otro valor (`cause: new Error(...)`) no cuenta: tiene que ser literalmente
700
+ el `result.error` del guard.
701
+
690
702
  Esta regla es type-aware. Usa TypeScript parser services para confirmar que el
691
703
  valor del guard y `Result.err` vienen de `@skapxd/result`. Por eso funciona con
692
704
  aliases, re-exports y tipos inferidos, sin depender solo del nombre importado en
693
- el archivo.
705
+ el archivo. Su punto ciego histórico —el `Result.err` escondido en un `if`
706
+ anidado— lo elimina `skapxd/no-nested-if` de raíz.
694
707
 
695
708
  ### `skapxd/await-requires-result`
696
709
 
@@ -846,6 +859,35 @@ Revisa imports estáticos (`import`), re-exports (`export ... from`) e imports
846
859
  dinámicos (`import(...)`). El remedio habitual es un alias de ruta (`@/...`) o
847
860
  acercar el módulo a quien lo usa.
848
861
 
862
+ ### `skapxd/no-nested-if`
863
+
864
+ Prohíbe un `if` dentro de otro `if` (en la misma función). Cada nivel de
865
+ anidación suma carga cognitiva para quien lee — y además crea puntos ciegos
866
+ para las demás reglas: un `Result.err` dentro de un if anidado quedaba fuera
867
+ del alcance de `result-error-requires-cause`. Esta regla elimina la categoría
868
+ completa de evasión en vez de parchear cada caso.
869
+
870
+ ```ts
871
+ // ❌ anidado: el lector mantiene dos condiciones en la cabeza
872
+ if (!response.ok) {
873
+ if (shouldReport) {
874
+ return Result.err({ cause: response.error, message: "...", type: "X" });
875
+ }
876
+ }
877
+
878
+ // ✅ retorno anticipado: una condición a la vez, camino feliz sin sangría
879
+ if (!response.ok && shouldReport) {
880
+ return Result.err({ cause: response.error, message: "...", type: "X" });
881
+ }
882
+
883
+ // ✅ o match() si son variantes de un mismo valor
884
+ ```
885
+
886
+ No cuenta como anidación: la cadena `else if` (es secuencia, no anidación), y
887
+ una función definida dentro del `if` (unidad cognitiva aparte). El propio
888
+ código de este plugin se aplanó con retorno anticipado al activar la regla —
889
+ cinco casos, todos quedaron más legibles.
890
+
849
891
  ### `skapxd/no-default-export`
850
892
 
851
893
  Prohíbe `export default` (incluida la forma `export { x as default }`). Con
@@ -1085,6 +1127,53 @@ const label = match(status)
1085
1127
  .exhaustive();
1086
1128
  ```
1087
1129
 
1130
+ ### `skapxd/prefer-abort-signal`
1131
+
1132
+ Dentro de un `useEffect`/`useLayoutEffect`, los listeners se limpian con
1133
+ `AbortController`, no con `removeEventListener` manual:
1134
+
1135
+ ```ts
1136
+ // ❌ registro y limpieza espejados a mano
1137
+ useEffect(() => {
1138
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
1139
+ media.addEventListener("change", onSystemChange);
1140
+ return () => media.removeEventListener("change", onSystemChange);
1141
+ }, [settings]);
1142
+
1143
+ // ✅ un AbortController por efecto
1144
+ useEffect(() => {
1145
+ const controller = new AbortController();
1146
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
1147
+ media.addEventListener("change", onSystemChange, { signal: controller.signal });
1148
+ return () => controller.abort();
1149
+ }, [settings]);
1150
+ ```
1151
+
1152
+ Por qué: un solo `abort()` limpia **todos** los listeners del efecto (no hay
1153
+ que espejar cada `add` con su `remove`), y elimina el bug clásico de pasar una
1154
+ referencia distinta a `removeEventListener` (un `.bind()` o una arrow nueva)
1155
+ que deja el listener vivo para siempre.
1156
+
1157
+ Reporta dos cosas dentro del callback del efecto (incluidas sus funciones
1158
+ anidadas y el cleanup): `addEventListener` sin `signal` en las options, y
1159
+ cualquier `removeEventListener`. Fuera de un efecto la regla no opina.
1160
+
1161
+ Cuando las options no son un objeto literal, la verificación resuelve en
1162
+ capas:
1163
+
1164
+ 1. **Por scope**: `addEventListener("x", fn, opts)` sigue `opts` hasta su
1165
+ `const opts = {...}` y lo inspecciona — sin necesitar type-checking.
1166
+ 2. **Por tipo** (con `projectService`): si no hay inicializador visible (un
1167
+ parámetro, un import), pregunta al checker si el **tipo** declara `signal`;
1168
+ si ni el tipo la tiene, es imposible que llegue y se reporta.
1169
+ 3. Sin inicializador ni tipos: beneficio de la duda.
1170
+
1171
+ El boolean de capture (`addEventListener("x", fn, true)`) se reporta siempre:
1172
+ no puede traer `signal`.
1173
+
1174
+ `effectNames` permite cubrir wrappers propios (`["useEffect",
1175
+ "useLayoutEffect", "useIsomorphicEffect"]`).
1176
+
1088
1177
  ### `skapxd/no-jsx-ternary-null`
1089
1178
 
1090
1179
  Cuando renderizas JSX condicional y una rama del ternario es `null`, prefiere la
@@ -14,6 +14,7 @@ declare function createAstroConfigs(pluginReference: unknown): ({
14
14
  "skapxd/no-deep-relative-imports": string;
15
15
  "skapxd/no-default-export": string;
16
16
  "skapxd/no-emoji": string;
17
+ "skapxd/no-nested-if": string;
17
18
  "skapxd/no-promise-chain": string;
18
19
  "skapxd/no-try-catch": string;
19
20
  "skapxd/one-root-function-per-file": string;
@@ -31,6 +32,7 @@ declare function createAstroConfigs(pluginReference: unknown): ({
31
32
  "skapxd/no-deep-relative-imports": string;
32
33
  "skapxd/no-default-export": string;
33
34
  "skapxd/no-emoji": string;
35
+ "skapxd/no-nested-if": string;
34
36
  "skapxd/no-promise-chain": string;
35
37
  "skapxd/no-try-catch": string;
36
38
  "skapxd/one-root-function-per-file": string;
@@ -14,6 +14,7 @@ declare function createAstroConfigs(pluginReference: unknown): ({
14
14
  "skapxd/no-deep-relative-imports": string;
15
15
  "skapxd/no-default-export": string;
16
16
  "skapxd/no-emoji": string;
17
+ "skapxd/no-nested-if": string;
17
18
  "skapxd/no-promise-chain": string;
18
19
  "skapxd/no-try-catch": string;
19
20
  "skapxd/one-root-function-per-file": string;
@@ -31,6 +32,7 @@ declare function createAstroConfigs(pluginReference: unknown): ({
31
32
  "skapxd/no-deep-relative-imports": string;
32
33
  "skapxd/no-default-export": string;
33
34
  "skapxd/no-emoji": string;
35
+ "skapxd/no-nested-if": string;
34
36
  "skapxd/no-promise-chain": string;
35
37
  "skapxd/no-try-catch": string;
36
38
  "skapxd/one-root-function-per-file": string;
@@ -40,6 +40,7 @@ var baseRules = {
40
40
  "skapxd/no-deep-relative-imports": "error",
41
41
  "skapxd/no-default-export": "error",
42
42
  "skapxd/no-emoji": "error",
43
+ "skapxd/no-nested-if": "error",
43
44
  "skapxd/no-promise-chain": "error",
44
45
  "skapxd/no-try-catch": "error",
45
46
  "skapxd/one-root-function-per-file": "error",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/astro/index.ts","../../src/shared/configs/base-rules.ts","../../src/shared/configs/create-base-language-options.ts","../../src/shared/configs/create-typed-language-options.ts","../../src/astro/configs.ts"],"sourcesContent":["export { createAstroConfigs } from \"./configs\";\n","export const baseRules = {\n \"skapxd/no-ad-hoc-ok-result\": \"error\",\n \"skapxd/no-deep-relative-imports\": \"error\",\n \"skapxd/no-default-export\": \"error\",\n \"skapxd/no-emoji\": \"error\",\n \"skapxd/no-promise-chain\": \"error\",\n \"skapxd/no-try-catch\": \"error\",\n \"skapxd/one-root-function-per-file\": \"error\",\n \"skapxd/prefer-ts-pattern\": \"error\",\n \"skapxd/result-error-requires-cause\": \"error\",\n};\n","import tseslint from \"typescript-eslint\";\n\n// Variante sin type-checking: solo el parser, para presets que aplican a TS\n// pero no necesitan projectService (base, package, next/base, next/react).\nexport function createBaseLanguageOptions() {\n return {\n parser: tseslint.parser,\n };\n}\n","import tseslint from \"typescript-eslint\";\n\nexport function createTypedLanguageOptions() {\n return {\n // Sin el parser explícito, un consumidor que use solo estos presets\n // obtiene \"Parsing error\" en cada archivo TS (espree no parsea TS).\n parser: tseslint.parser,\n parserOptions: {\n projectService: true,\n },\n };\n}\n","import {\n baseRules,\n createBaseLanguageOptions,\n createTypedLanguageOptions,\n} from \"#/shared/configs\";\n\nexport function createAstroConfigs(pluginReference: unknown) {\n const baseLanguageOptions = createBaseLanguageOptions();\n const typedLanguageOptions = createTypedLanguageOptions();\n\n return [\n {\n files: [\"src/**/*.{ts,tsx}\"],\n languageOptions: baseLanguageOptions,\n name: \"skapxd/astro/base\",\n plugins: { skapxd: pluginReference },\n rules: baseRules,\n },\n // Los .astro no llevan parser propio: lo aporta eslint-plugin-astro,\n // que el consumidor debe tener configurado.\n {\n files: [\"src/**/*.astro\"],\n name: \"skapxd/astro/astro-files\",\n plugins: { skapxd: pluginReference },\n rules: baseRules,\n },\n {\n files: [\"src/**/*.{ts,tsx}\"],\n languageOptions: typedLanguageOptions,\n name: \"skapxd/astro/typescript\",\n plugins: { skapxd: pluginReference },\n rules: {\n \"skapxd/await-requires-result\": \"error\",\n \"skapxd/result-error-requires-cause\": \"error\",\n },\n },\n ];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAO,IAAM,YAAY;AAAA,EACvB,8BAA8B;AAAA,EAC9B,mCAAmC;AAAA,EACnC,4BAA4B;AAAA,EAC5B,mBAAmB;AAAA,EACnB,2BAA2B;AAAA,EAC3B,uBAAuB;AAAA,EACvB,qCAAqC;AAAA,EACrC,4BAA4B;AAAA,EAC5B,sCAAsC;AACxC;;;ACVA,+BAAqB;AAId,SAAS,4BAA4B;AAC1C,SAAO;AAAA,IACL,QAAQ,yBAAAA,QAAS;AAAA,EACnB;AACF;;;ACRA,IAAAC,4BAAqB;AAEd,SAAS,6BAA6B;AAC3C,SAAO;AAAA;AAAA;AAAA,IAGL,QAAQ,0BAAAC,QAAS;AAAA,IACjB,eAAe;AAAA,MACb,gBAAgB;AAAA,IAClB;AAAA,EACF;AACF;;;ACLO,SAAS,mBAAmB,iBAA0B;AAC3D,QAAM,sBAAsB,0BAA0B;AACtD,QAAM,uBAAuB,2BAA2B;AAExD,SAAO;AAAA,IACL;AAAA,MACE,OAAO,CAAC,mBAAmB;AAAA,MAC3B,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,IACT;AAAA;AAAA;AAAA,IAGA;AAAA,MACE,OAAO,CAAC,gBAAgB;AAAA,MACxB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,IACT;AAAA,IACA;AAAA,MACE,OAAO,CAAC,mBAAmB;AAAA,MAC3B,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,QACL,gCAAgC;AAAA,QAChC,sCAAsC;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AACF;","names":["tseslint","import_typescript_eslint","tseslint"]}
1
+ {"version":3,"sources":["../../src/astro/index.ts","../../src/shared/configs/base-rules.ts","../../src/shared/configs/create-base-language-options.ts","../../src/shared/configs/create-typed-language-options.ts","../../src/astro/configs.ts"],"sourcesContent":["export { createAstroConfigs } from \"./configs\";\n","export const baseRules = {\n \"skapxd/no-ad-hoc-ok-result\": \"error\",\n \"skapxd/no-deep-relative-imports\": \"error\",\n \"skapxd/no-default-export\": \"error\",\n \"skapxd/no-emoji\": \"error\",\n \"skapxd/no-nested-if\": \"error\",\n \"skapxd/no-promise-chain\": \"error\",\n \"skapxd/no-try-catch\": \"error\",\n \"skapxd/one-root-function-per-file\": \"error\",\n \"skapxd/prefer-ts-pattern\": \"error\",\n \"skapxd/result-error-requires-cause\": \"error\",\n};\n","import tseslint from \"typescript-eslint\";\n\n// Variante sin type-checking: solo el parser, para presets que aplican a TS\n// pero no necesitan projectService (base, package, next/base, next/react).\nexport function createBaseLanguageOptions() {\n return {\n parser: tseslint.parser,\n };\n}\n","import tseslint from \"typescript-eslint\";\n\nexport function createTypedLanguageOptions() {\n return {\n // Sin el parser explícito, un consumidor que use solo estos presets\n // obtiene \"Parsing error\" en cada archivo TS (espree no parsea TS).\n parser: tseslint.parser,\n parserOptions: {\n projectService: true,\n },\n };\n}\n","import {\n baseRules,\n createBaseLanguageOptions,\n createTypedLanguageOptions,\n} from \"#/shared/configs\";\n\nexport function createAstroConfigs(pluginReference: unknown) {\n const baseLanguageOptions = createBaseLanguageOptions();\n const typedLanguageOptions = createTypedLanguageOptions();\n\n return [\n {\n files: [\"src/**/*.{ts,tsx}\"],\n languageOptions: baseLanguageOptions,\n name: \"skapxd/astro/base\",\n plugins: { skapxd: pluginReference },\n rules: baseRules,\n },\n // Los .astro no llevan parser propio: lo aporta eslint-plugin-astro,\n // que el consumidor debe tener configurado.\n {\n files: [\"src/**/*.astro\"],\n name: \"skapxd/astro/astro-files\",\n plugins: { skapxd: pluginReference },\n rules: baseRules,\n },\n {\n files: [\"src/**/*.{ts,tsx}\"],\n languageOptions: typedLanguageOptions,\n name: \"skapxd/astro/typescript\",\n plugins: { skapxd: pluginReference },\n rules: {\n \"skapxd/await-requires-result\": \"error\",\n \"skapxd/result-error-requires-cause\": \"error\",\n },\n },\n ];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAO,IAAM,YAAY;AAAA,EACvB,8BAA8B;AAAA,EAC9B,mCAAmC;AAAA,EACnC,4BAA4B;AAAA,EAC5B,mBAAmB;AAAA,EACnB,uBAAuB;AAAA,EACvB,2BAA2B;AAAA,EAC3B,uBAAuB;AAAA,EACvB,qCAAqC;AAAA,EACrC,4BAA4B;AAAA,EAC5B,sCAAsC;AACxC;;;ACXA,+BAAqB;AAId,SAAS,4BAA4B;AAC1C,SAAO;AAAA,IACL,QAAQ,yBAAAA,QAAS;AAAA,EACnB;AACF;;;ACRA,IAAAC,4BAAqB;AAEd,SAAS,6BAA6B;AAC3C,SAAO;AAAA;AAAA;AAAA,IAGL,QAAQ,0BAAAC,QAAS;AAAA,IACjB,eAAe;AAAA,MACb,gBAAgB;AAAA,IAClB;AAAA,EACF;AACF;;;ACLO,SAAS,mBAAmB,iBAA0B;AAC3D,QAAM,sBAAsB,0BAA0B;AACtD,QAAM,uBAAuB,2BAA2B;AAExD,SAAO;AAAA,IACL;AAAA,MACE,OAAO,CAAC,mBAAmB;AAAA,MAC3B,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,IACT;AAAA;AAAA;AAAA,IAGA;AAAA,MACE,OAAO,CAAC,gBAAgB;AAAA,MACxB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,IACT;AAAA,IACA;AAAA,MACE,OAAO,CAAC,mBAAmB;AAAA,MAC3B,iBAAiB;AAAA,MACjB,MAAM;AAAA,MACN,SAAS,EAAE,QAAQ,gBAAgB;AAAA,MACnC,OAAO;AAAA,QACL,gCAAgC;AAAA,QAChC,sCAAsC;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AACF;","names":["tseslint","import_typescript_eslint","tseslint"]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createAstroConfigs
3
- } from "../chunk-5P6OHVFD.mjs";
4
- import "../chunk-EGNXI5HL.mjs";
3
+ } from "../chunk-3NM7FEIN.mjs";
4
+ import "../chunk-J472YWOD.mjs";
5
5
  export {
6
6
  createAstroConfigs
7
7
  };
@@ -2,7 +2,7 @@ import {
2
2
  baseRules,
3
3
  createBaseLanguageOptions,
4
4
  createTypedLanguageOptions
5
- } from "./chunk-EGNXI5HL.mjs";
5
+ } from "./chunk-J472YWOD.mjs";
6
6
 
7
7
  // src/astro/configs.ts
8
8
  function createAstroConfigs(pluginReference) {
@@ -40,4 +40,4 @@ function createAstroConfigs(pluginReference) {
40
40
  export {
41
41
  createAstroConfigs
42
42
  };
43
- //# sourceMappingURL=chunk-5P6OHVFD.mjs.map
43
+ //# sourceMappingURL=chunk-3NM7FEIN.mjs.map
@@ -644,14 +644,16 @@ var asyncFunctionsReturnResult = {
644
644
  return;
645
645
  }
646
646
  const returnType = node.returnType?.typeAnnotation;
647
+ const missingReturnTypeIsReportable = options.checkMissingReturnType || containsCallNamed(node.body, options.checkMissingReturnTypeWhenCallNames);
648
+ if (!returnType && missingReturnTypeIsReportable) {
649
+ context.report({
650
+ data: { name: functionName },
651
+ messageId: "missingReturnType",
652
+ node: reportNode
653
+ });
654
+ return;
655
+ }
647
656
  if (!returnType) {
648
- if (options.checkMissingReturnType || containsCallNamed(node.body, options.checkMissingReturnTypeWhenCallNames)) {
649
- context.report({
650
- data: { name: functionName },
651
- messageId: "missingReturnType",
652
- node: reportNode
653
- });
654
- }
655
657
  return;
656
658
  }
657
659
  if (isSkapxdResultReturnType(returnType)) {
@@ -813,14 +815,13 @@ function getAwaitScopeName(node) {
813
815
  function getEnclosingTrySafeCall(node, trySafeCallNames) {
814
816
  let currentNode = node.parent;
815
817
  while (currentNode) {
816
- if (isFunctionNode(currentNode)) {
817
- const parent = currentNode.parent;
818
- if (parent?.type === "CallExpression" && parent.arguments.includes(currentNode) && isCalleeNamed(parent.callee, trySafeCallNames)) {
819
- return parent;
820
- }
821
- return null;
818
+ if (!isFunctionNode(currentNode)) {
819
+ currentNode = currentNode.parent;
820
+ continue;
822
821
  }
823
- currentNode = currentNode.parent;
822
+ const parent = currentNode.parent;
823
+ const isTrySafeArgument = parent?.type === "CallExpression" && parent.arguments.includes(currentNode) && isCalleeNamed(parent.callee, trySafeCallNames);
824
+ return isTrySafeArgument ? parent : null;
824
825
  }
825
826
  return null;
826
827
  }
@@ -954,6 +955,18 @@ var awaitRequiresResult = {
954
955
  }
955
956
  };
956
957
 
958
+ // src/utils/get-error-member-object.ts
959
+ function getErrorMemberObject(node) {
960
+ const unwrappedNode = unwrapExpression(node);
961
+ if (unwrappedNode.type !== "MemberExpression" || unwrappedNode.object.type !== "Identifier" || !isMemberPropertyNamed(unwrappedNode, "error")) {
962
+ return null;
963
+ }
964
+ return {
965
+ name: unwrappedNode.object.name,
966
+ node: unwrappedNode.object
967
+ };
968
+ }
969
+
957
970
  // src/utils/get-ok-member-object.ts
958
971
  function getOkMemberObject(node) {
959
972
  const unwrappedNode = unwrapExpression(node);
@@ -1002,6 +1015,9 @@ function getResultCheckArgument(node, methodName) {
1002
1015
  // src/utils/get-failed-result-guard.ts
1003
1016
  function getFailedResultGuard(node) {
1004
1017
  const unwrappedNode = unwrapExpression(node);
1018
+ if (unwrappedNode.type === "MemberExpression") {
1019
+ return getErrorMemberObject(unwrappedNode);
1020
+ }
1005
1021
  if (unwrappedNode.type === "UnaryExpression" && unwrappedNode.operator === "!") {
1006
1022
  return getOkMemberObject(unwrappedNode.argument) ?? getResultCheckArgument(unwrappedNode.argument, "isOk");
1007
1023
  }
@@ -1119,10 +1135,7 @@ var resultErrorRequiresCause = {
1119
1135
  if (!isSkapxdResultErrCall(resultErrCall, typeContext)) {
1120
1136
  continue;
1121
1137
  }
1122
- if (resultErrCall.arguments.length === 0) {
1123
- continue;
1124
- }
1125
- if (resultErrPreservesCause(resultErrCall.arguments[0], resultGuard.name)) {
1138
+ if (resultErrCall.arguments.length > 0 && resultErrPreservesCause(resultErrCall.arguments[0], resultGuard.name)) {
1126
1139
  continue;
1127
1140
  }
1128
1141
  context.report({
@@ -1697,6 +1710,134 @@ var noTryCatch = {
1697
1710
  }
1698
1711
  };
1699
1712
 
1713
+ // src/utils/get-prefer-abort-signal-options.ts
1714
+ function getPreferAbortSignalOptions(options = {}) {
1715
+ return {
1716
+ allowFilePatterns: options.allowFilePatterns ?? [],
1717
+ effectNames: options.effectNames ?? ["useEffect", "useLayoutEffect"]
1718
+ };
1719
+ }
1720
+
1721
+ // src/utils/get-variable-initializer.ts
1722
+ function getVariableInitializer(identifier, scope) {
1723
+ let current = scope;
1724
+ while (current) {
1725
+ const variable = current.variables.find(
1726
+ (candidate) => candidate.name === identifier.name
1727
+ );
1728
+ if (variable) {
1729
+ const definition = variable.defs[0];
1730
+ return definition?.node?.type === "VariableDeclarator" ? definition.node.init : null;
1731
+ }
1732
+ current = current.upper;
1733
+ }
1734
+ return null;
1735
+ }
1736
+
1737
+ // src/utils/object-expression-has-signal.ts
1738
+ function objectExpressionHasSignal(objectExpression) {
1739
+ return objectExpression.properties.some(
1740
+ (property) => property.type === "SpreadElement" || isPropertyKeyNamed(property, "signal")
1741
+ );
1742
+ }
1743
+
1744
+ // src/utils/has-abort-signal-option.ts
1745
+ function hasAbortSignalOption(callExpression, sourceCode, typeContext) {
1746
+ const options = callExpression.arguments[2];
1747
+ if (!options) {
1748
+ return false;
1749
+ }
1750
+ if (options.type === "Literal") {
1751
+ return false;
1752
+ }
1753
+ if (options.type === "ObjectExpression") {
1754
+ return objectExpressionHasSignal(options);
1755
+ }
1756
+ const initializer = options.type === "Identifier" ? getVariableInitializer(options, sourceCode.getScope(options)) : null;
1757
+ if (initializer?.type === "ObjectExpression") {
1758
+ return objectExpressionHasSignal(initializer);
1759
+ }
1760
+ if (typeContext) {
1761
+ const type = typeContext.services.getTypeAtLocation(options);
1762
+ return Boolean(typeContext.checker.getPropertyOfType(type, "signal"));
1763
+ }
1764
+ return true;
1765
+ }
1766
+
1767
+ // src/utils/is-inside-effect-callback.ts
1768
+ function isInsideEffectCallback(node, effectNames) {
1769
+ let current = node.parent;
1770
+ while (current) {
1771
+ const call = isFunctionNode(current) ? current.parent : null;
1772
+ if (call?.type === "CallExpression" && call.arguments[0] === current && isCalleeNamed(call.callee, effectNames)) {
1773
+ return true;
1774
+ }
1775
+ current = current.parent;
1776
+ }
1777
+ return false;
1778
+ }
1779
+
1780
+ // src/rules/prefer-abort-signal.ts
1781
+ var preferAbortSignal = {
1782
+ meta: {
1783
+ type: "suggestion",
1784
+ docs: {
1785
+ description: "En efectos de React, los listeners se limpian con AbortController, no con removeEventListener."
1786
+ },
1787
+ messages: {
1788
+ addWithoutSignal: "Este addEventListener vive en un efecto sin `signal`. Crea `const controller = new AbortController()`, pasa `{ signal: controller.signal }` como tercer argumento y limpia con `return () => controller.abort()`: un solo abort cubre todos los listeners del efecto.",
1789
+ removeInsteadOfAbort: "No quites listeners a mano en el cleanup: registra con `{ signal: controller.signal }` y reemplaza este removeEventListener por `return () => controller.abort()`. Evita el bug clasico de pasar una referencia distinta al remover."
1790
+ },
1791
+ schema: [
1792
+ {
1793
+ additionalProperties: false,
1794
+ properties: {
1795
+ allowFilePatterns: {
1796
+ items: { type: "string" },
1797
+ type: "array"
1798
+ },
1799
+ effectNames: {
1800
+ items: { type: "string" },
1801
+ type: "array"
1802
+ }
1803
+ },
1804
+ type: "object"
1805
+ }
1806
+ ]
1807
+ },
1808
+ create(context) {
1809
+ const options = getPreferAbortSignalOptions(context.options[0]);
1810
+ const filename = context.filename ?? context.getFilename();
1811
+ const sourceCode = context.sourceCode ?? context.getSourceCode();
1812
+ const typeContext = getTypeContext(context);
1813
+ if (matchesAnyGlob(filename, options.allowFilePatterns)) {
1814
+ return {};
1815
+ }
1816
+ return {
1817
+ CallExpression(node) {
1818
+ if (node.callee?.type !== "MemberExpression") {
1819
+ return;
1820
+ }
1821
+ const isAdd = isMemberPropertyNamed(node.callee, "addEventListener");
1822
+ const isRemove = isMemberPropertyNamed(node.callee, "removeEventListener");
1823
+ if (!isAdd && !isRemove) {
1824
+ return;
1825
+ }
1826
+ if (!isInsideEffectCallback(node, options.effectNames)) {
1827
+ return;
1828
+ }
1829
+ if (isRemove) {
1830
+ context.report({ messageId: "removeInsteadOfAbort", node });
1831
+ return;
1832
+ }
1833
+ if (!hasAbortSignalOption(node, sourceCode, typeContext)) {
1834
+ context.report({ messageId: "addWithoutSignal", node });
1835
+ }
1836
+ }
1837
+ };
1838
+ }
1839
+ };
1840
+
1700
1841
  // src/rules/prefer-ts-pattern.ts
1701
1842
  var preferTsPattern = {
1702
1843
  meta: {
@@ -1768,6 +1909,67 @@ var noJsxTernaryNull = {
1768
1909
  }
1769
1910
  };
1770
1911
 
1912
+ // src/utils/get-no-nested-if-options.ts
1913
+ function getNoNestedIfOptions(options = {}) {
1914
+ return {
1915
+ allowFilePatterns: options.allowFilePatterns ?? []
1916
+ };
1917
+ }
1918
+
1919
+ // src/utils/is-nested-if-statement.ts
1920
+ function isNestedIfStatement(node) {
1921
+ if (node.parent?.type === "IfStatement" && node.parent.alternate === node) {
1922
+ return false;
1923
+ }
1924
+ let current = node.parent;
1925
+ while (current && !isFunctionNode(current)) {
1926
+ if (current.type === "IfStatement") {
1927
+ return true;
1928
+ }
1929
+ current = current.parent;
1930
+ }
1931
+ return false;
1932
+ }
1933
+
1934
+ // src/rules/no-nested-if.ts
1935
+ var noNestedIf = {
1936
+ meta: {
1937
+ type: "suggestion",
1938
+ docs: {
1939
+ description: "Prohibe if anidados; usa retorno anticipado (guard clauses) o match() de ts-pattern."
1940
+ },
1941
+ messages: {
1942
+ noNestedIf: "No anides un if dentro de otro: cada nivel suma carga cognitiva y crea puntos ciegos para las demas reglas. Aplana con retorno anticipado (`if (!x) return ...;` y sigue el camino feliz) o decide con `match()` de ts-pattern si son variantes de un mismo valor."
1943
+ },
1944
+ schema: [
1945
+ {
1946
+ additionalProperties: false,
1947
+ properties: {
1948
+ allowFilePatterns: {
1949
+ items: { type: "string" },
1950
+ type: "array"
1951
+ }
1952
+ },
1953
+ type: "object"
1954
+ }
1955
+ ]
1956
+ },
1957
+ create(context) {
1958
+ const options = getNoNestedIfOptions(context.options[0]);
1959
+ const filename = context.filename ?? context.getFilename();
1960
+ if (matchesAnyGlob(filename, options.allowFilePatterns)) {
1961
+ return {};
1962
+ }
1963
+ return {
1964
+ IfStatement(node) {
1965
+ if (isNestedIfStatement(node)) {
1966
+ context.report({ messageId: "noNestedIf", node });
1967
+ }
1968
+ }
1969
+ };
1970
+ }
1971
+ };
1972
+
1771
1973
  // src/utils/is-promise-type.ts
1772
1974
  function isPromiseType(type, typeContext) {
1773
1975
  return typeContext.checker.getPromisedTypeOfPromise(type) !== void 0;
@@ -1810,11 +2012,11 @@ var noPromiseChain = {
1810
2012
  if (!method) {
1811
2013
  return;
1812
2014
  }
1813
- if (typeContext) {
1814
- const type = typeContext.services.getTypeAtLocation(callee.object);
1815
- if (!isPromiseType(type, typeContext)) {
1816
- return;
1817
- }
2015
+ if (typeContext && !isPromiseType(
2016
+ typeContext.services.getTypeAtLocation(callee.object),
2017
+ typeContext
2018
+ )) {
2019
+ return;
1818
2020
  }
1819
2021
  context.report({
1820
2022
  data: { method },
@@ -1850,12 +2052,14 @@ var rules = {
1850
2052
  "no-tunnel-props": noTunnelProps,
1851
2053
  "no-functions-inside-components": noFunctionsInsideComponents,
1852
2054
  "no-try-catch": noTryCatch,
2055
+ "prefer-abort-signal": preferAbortSignal,
1853
2056
  "prefer-ts-pattern": preferTsPattern,
1854
2057
  "no-jsx-ternary-null": noJsxTernaryNull,
2058
+ "no-nested-if": noNestedIf,
1855
2059
  "no-promise-chain": noPromiseChain
1856
2060
  };
1857
2061
 
1858
2062
  export {
1859
2063
  rules
1860
2064
  };
1861
- //# sourceMappingURL=chunk-O4RTJGEU.mjs.map
2065
+ //# sourceMappingURL=chunk-EXMF54EM.mjs.map