@skapxd/eslint-opinionated 0.17.0 → 1.0.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
@@ -831,6 +831,7 @@ de cada regla):
831
831
  | `nest-validation-pipe-config` | `allowFilePatterns` (globs), `requiredPipeOptions` (default `["transform", "whitelist"]`) |
832
832
  | `no-deep-relative-imports` | `maxDepth` |
833
833
  | `no-default-export` | `allowFilePatterns` (globs, aditivos a los integrados) |
834
+ | `no-anonymous-condition` | `allowFilePatterns` (globs), `maxMemberDepth` (default `2`), `allowTypePredicates` (default `true`, type-aware) |
834
835
  | `no-else` | `allowFilePatterns` (globs) |
835
836
  | `no-emoji` | `allowFilePatterns` (globs) |
836
837
  | `no-explicit-any` | las de la regla original de typescript-eslint (`fixToUnknown`, ...) |
@@ -879,6 +880,7 @@ matchea en cualquier carpeta). Las 7 reglas restantes no tienen opciones: su
879
880
  | `skapxd/nest-no-swagger-in-controllers` | Los controllers no se llenan de decoradores de swagger; el plugin introspecciona los DTOs. Preset `nest`. |
880
881
  | `skapxd/nest-requires-swagger-plugin` | `nest-cli.json` debe tener el plugin `@nestjs/swagger`: la premisa de las reglas de swagger, verificada. Preset `nest`. |
881
882
  | `skapxd/nest-validation-pipe-config` | Todo `new ValidationPipe` configura `transform` y `whitelist`: la premisa de las reglas de DTOs. Preset `nest`. |
883
+ | `skapxd/no-anonymous-condition` | El `if` solo acepta condiciones ya nombradas; todo cómputo (llamada, comparación, `&&`/`||`) se extrae a una `const` con nombre semántico. **Opt-in: no está en ningún preset.** |
882
884
  | `skapxd/no-deep-relative-imports` | Limita la profundidad de los imports relativos (`../`). |
883
885
  | `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. |
884
886
  | `skapxd/no-else` | Prohíbe `else`/`else if`: el else es el estado sin nombre. Retorno anticipado, ternario simple o `match()`. |
@@ -1197,8 +1199,9 @@ con las mismas cinco formas de guard.
1197
1199
  > }
1198
1200
  > ```
1199
1201
  >
1200
- > (`skapxd/await-requires-try-safe` es el nombre anterior; sigue funcionando
1201
- > como alias deprecado y se eliminará en una versión futura.)
1202
+ > (`skapxd/await-requires-try-safe` fue el nombre anterior; el alias se
1203
+ > eliminó en la v1.0.0 si tu config lo menciona, renómbralo a
1204
+ > `skapxd/await-requires-result`: mismo comportamiento, mismas opciones.)
1202
1205
 
1203
1206
  Hay dos caminos válidos, y la regla recomienda el primero:
1204
1207
 
@@ -1660,6 +1663,57 @@ por indirección tampoco escapa. Solo aplica a clases con `@Controller`
1660
1663
  (configurable con `controllerDecoratorNames` para decoradores propios); los
1661
1664
  services retornan Result con orgullo — ese es el dominio.
1662
1665
 
1666
+ ### `skapxd/no-anonymous-condition`
1667
+
1668
+ La hermana de `no-else`: esa nombra los **caminos**, esta nombra la
1669
+ **pregunta**. Un `if` cuya condición es un cómputo evalúa un valor anónimo
1670
+ cuyo significado vive solo en la cabeza de quien lo escribió; la regla exige
1671
+ bautizarlo (el refactor "introduce explaining variable" de Fowler, como ley):
1672
+
1673
+ ```ts
1674
+ if (matchesAnyGlob(filename, options.allowFilePatterns)) { ... } // ❌ ¿qué significa que matchee?
1675
+
1676
+ const esArchivoExento = matchesAnyGlob(filename, options.allowFilePatterns);
1677
+ if (esArchivoExento) { ... } // ✅ la decisión se lee como prosa
1678
+ ```
1679
+
1680
+ Lo **ya nombrado** no se extrae (la lista blanca — extraerlo sería ceremonia
1681
+ sin información):
1682
+
1683
+ - Variables y sus negaciones: `isReady`, `!isReady`, `!!isReady`.
1684
+ - Accesos a propiedad hasta `maxMemberDepth` saltos (contando puntos desde
1685
+ la base como nivel 0: `result.ok` → 1, `options.rules.flag` → 2; default
1686
+ `2`) y sus negaciones — incluido el encadenamiento opcional (`config?.flag`).
1687
+ - Comparaciones contra literal booleano o nullish (`x.ok === false`,
1688
+ `x == null`, `x !== undefined`): la escritura explícita de la
1689
+ afirmación/negación/presencia. Cubre las formas oficiales del guard de
1690
+ Result, que `result-error-requires-cause/handling` necesitan ver intactas.
1691
+ - **Type guards demostrados por la firma** (`allowTypePredicates`, default
1692
+ `true`): `if (isFunctionNode(x))` pasa cuando la firma declara
1693
+ `x is FunctionNode` — el type-checker lo demuestra (evidencia, no
1694
+ convención de nombre: una `isX(...)` que devuelve `boolean` a secas sí se
1695
+ extrae). Requiere type info; sin parser services no hay evidencia y toda
1696
+ llamada exige nombre. `Result.isErr(x)` pasa por esta vía: es un type
1697
+ predicate real.
1698
+
1699
+ Lo que **sí dispara**: llamadas, comparaciones (`a.length <= b.max`,
1700
+ `status === "ready"`), combinaciones `&&`/`||` y aritmética
1701
+ (`if (total % 2)`). La extracción directa a `const` conserva el narrowing
1702
+ (TS 4.4+, aliased conditions).
1703
+
1704
+ **Opt-in deliberado: no está en ningún preset.** La calibración contra 4
1705
+ proyectos reales (2026-06-12) midió 473/95/308 hallazgos en tres backends
1706
+ NestJS en producción y 44 en un front pequeño — señal genuina en la muestra
1707
+ revisada, pero un orden de magnitud más invasiva que cualquier regla de las
1708
+ bases. Actívala por proyecto (o por carpeta, estilo ola 3 del playbook de
1709
+ adopción):
1710
+
1711
+ ```js
1712
+ rules: {
1713
+ "skapxd/no-anonymous-condition": "error",
1714
+ }
1715
+ ```
1716
+
1663
1717
  ### `skapxd/no-deep-relative-imports`
1664
1718
 
1665
1719
  Limita cuántos niveles puede subir un import relativo. Por defecto **prohíbe
@@ -2652,6 +2652,157 @@ var noDefaultExport = {
2652
2652
  }
2653
2653
  };
2654
2654
 
2655
+ // src/utils/call-has-type-predicate.ts
2656
+ function callHasTypePredicate(node, parserServices) {
2657
+ const tsNode = parserServices?.esTreeNodeToTSNodeMap?.get(node);
2658
+ const program = parserServices?.program;
2659
+ if (tsNode === void 0 || program === null || program === void 0) {
2660
+ return false;
2661
+ }
2662
+ const checker = program.getTypeChecker();
2663
+ const signature = checker.getResolvedSignature(
2664
+ tsNode
2665
+ );
2666
+ if (signature === void 0) {
2667
+ return false;
2668
+ }
2669
+ return checker.getTypePredicateOfSignature(signature) !== void 0;
2670
+ }
2671
+
2672
+ // src/utils/get-member-chain-depth.ts
2673
+ function getMemberChainDepth(node) {
2674
+ let current = node.type === "ChainExpression" && node.expression !== void 0 ? node.expression : node;
2675
+ let depth = 0;
2676
+ while (current.type === "MemberExpression") {
2677
+ depth += 1;
2678
+ if (current.object === void 0) {
2679
+ return null;
2680
+ }
2681
+ current = current.object;
2682
+ }
2683
+ if (current.type === "Identifier" || current.type === "ThisExpression") {
2684
+ return depth;
2685
+ }
2686
+ return null;
2687
+ }
2688
+
2689
+ // src/utils/get-no-anonymous-condition-options.ts
2690
+ function getNoAnonymousConditionOptions(options = {}) {
2691
+ return {
2692
+ allowFilePatterns: options.allowFilePatterns ?? [],
2693
+ // Permitir llamadas cuya firma el type-checker demuestra como type
2694
+ // predicate (`x is T`). Sin type info, no hay evidencia y no aplica.
2695
+ allowTypePredicates: options.allowTypePredicates ?? true,
2696
+ // Saltos de propiedad permitidos en la lista blanca, contando desde la
2697
+ // base como nivel 0: `result.ok` → 1, `options.rules.flag` → 2.
2698
+ maxMemberDepth: options.maxMemberDepth ?? 2
2699
+ };
2700
+ }
2701
+
2702
+ // src/utils/is-guard-literal.ts
2703
+ function isGuardLiteral(node) {
2704
+ if (node.type === "Literal") {
2705
+ return typeof node.value === "boolean" || node.value === null;
2706
+ }
2707
+ return node.type === "Identifier" && node.name === "undefined";
2708
+ }
2709
+
2710
+ // src/utils/is-literal-guard-comparison.ts
2711
+ var guardOperators = /* @__PURE__ */ new Set(["===", "!==", "==", "!="]);
2712
+ function isLiteralGuardComparison(node, maxMemberDepth) {
2713
+ if (node.type !== "BinaryExpression") {
2714
+ return false;
2715
+ }
2716
+ if (node.operator === void 0 || !guardOperators.has(node.operator)) {
2717
+ return false;
2718
+ }
2719
+ const { left, right } = node;
2720
+ if (left === void 0 || right === void 0) {
2721
+ return false;
2722
+ }
2723
+ const leftIsGuard = isGuardLiteral(left);
2724
+ const rightIsGuard = isGuardLiteral(right);
2725
+ if (!leftIsGuard && !rightIsGuard) {
2726
+ return false;
2727
+ }
2728
+ const namedSide = leftIsGuard ? right : left;
2729
+ const depth = getMemberChainDepth(namedSide);
2730
+ return depth !== null && depth <= maxMemberDepth;
2731
+ }
2732
+
2733
+ // src/utils/unwrap-negations.ts
2734
+ function unwrapNegations(node) {
2735
+ let current = node;
2736
+ while (current.type === "UnaryExpression" && current.operator === "!" && current.argument !== void 0) {
2737
+ current = current.argument;
2738
+ }
2739
+ return current;
2740
+ }
2741
+
2742
+ // src/rules/no-anonymous-condition.ts
2743
+ var noAnonymousCondition = {
2744
+ meta: {
2745
+ type: "suggestion",
2746
+ docs: {
2747
+ description: "La condicion sin nombre: el if solo acepta condiciones ya nombradas (variables, accesos acotados, type guards demostrados); todo computo se extrae a una const con nombre semantico."
2748
+ },
2749
+ messages: {
2750
+ anonymousCondition: "La condicion sin nombre: este if evalua un computo cuyo significado el lector debe deducir. Extraelo a una const con nombre semantico y la decision se lee como prosa: `const esArchivoExento = matchesAnyGlob(archivo, patrones); if (esArchivoExento) ...`. La extraccion directa a const conserva el narrowing (TS 4.4+). Lo ya-nombrado no se extrae: variables (`isReady`), accesos como `result.ok` (hasta {{maxMemberDepth}} saltos), sus negaciones (`!result.ok`), comparaciones con literal booleano o nullish (`x.ok === false`, `x == null`) y type guards que la firma demuestra (`x is T`)."
2751
+ },
2752
+ schema: [
2753
+ {
2754
+ additionalProperties: false,
2755
+ properties: {
2756
+ allowFilePatterns: {
2757
+ items: { type: "string" },
2758
+ type: "array"
2759
+ },
2760
+ allowTypePredicates: {
2761
+ type: "boolean"
2762
+ },
2763
+ maxMemberDepth: {
2764
+ type: "number"
2765
+ }
2766
+ },
2767
+ type: "object"
2768
+ }
2769
+ ]
2770
+ },
2771
+ create(context) {
2772
+ const options = getNoAnonymousConditionOptions(
2773
+ context.options[0]
2774
+ );
2775
+ const filename = context.filename ?? context.getFilename?.() ?? "";
2776
+ if (matchesAnyGlob(filename, options.allowFilePatterns)) {
2777
+ return {};
2778
+ }
2779
+ return {
2780
+ IfStatement(node) {
2781
+ const condition = unwrapNegations(node.test);
2782
+ if (condition.type === "Literal") {
2783
+ return;
2784
+ }
2785
+ const depth = getMemberChainDepth(condition);
2786
+ if (depth !== null && depth <= options.maxMemberDepth) {
2787
+ return;
2788
+ }
2789
+ if (isLiteralGuardComparison(condition, options.maxMemberDepth)) {
2790
+ return;
2791
+ }
2792
+ const isProvenTypeGuard = condition.type === "CallExpression" && options.allowTypePredicates && callHasTypePredicate(condition, context.sourceCode?.parserServices);
2793
+ if (isProvenTypeGuard) {
2794
+ return;
2795
+ }
2796
+ context.report({
2797
+ data: { maxMemberDepth: String(options.maxMemberDepth) },
2798
+ messageId: "anonymousCondition",
2799
+ node: node.test
2800
+ });
2801
+ }
2802
+ };
2803
+ }
2804
+ };
2805
+
2655
2806
  // src/utils/get-no-else-options.ts
2656
2807
  function getNoElseOptions(options = {}) {
2657
2808
  return {
@@ -4065,15 +4216,6 @@ var rules = {
4065
4216
  "async-functions-return-result": asyncFunctionsReturnResult,
4066
4217
  "no-ad-hoc-ok-result": noAdHocOkResult,
4067
4218
  "await-requires-result": awaitRequiresResult,
4068
- // Alias deprecado del nombre anterior; se elimina en una versión futura.
4069
- "await-requires-try-safe": {
4070
- ...awaitRequiresResult,
4071
- meta: {
4072
- ...awaitRequiresResult.meta,
4073
- deprecated: true,
4074
- replacedBy: ["skapxd/await-requires-result"]
4075
- }
4076
- },
4077
4219
  "result-error-requires-cause": resultErrorRequiresCause,
4078
4220
  "result-error-requires-handling": resultErrorRequiresHandling,
4079
4221
  "max-hook-size": maxHookSize,
@@ -4086,6 +4228,7 @@ var rules = {
4086
4228
  "nest-no-swagger-in-controllers": nestNoSwaggerInControllers,
4087
4229
  "nest-requires-swagger-plugin": nestRequiresSwaggerPlugin,
4088
4230
  "nest-validation-pipe-config": nestValidationPipeConfig,
4231
+ "no-anonymous-condition": noAnonymousCondition,
4089
4232
  "no-deep-relative-imports": noDeepRelativeImports,
4090
4233
  "no-default-export": noDefaultExport,
4091
4234
  "no-else": noElse,
@@ -4116,4 +4259,4 @@ var rules = {
4116
4259
  export {
4117
4260
  rules
4118
4261
  };
4119
- //# sourceMappingURL=chunk-PD77UDUY.mjs.map
4262
+ //# sourceMappingURL=chunk-S2MDPOWE.mjs.map