@skapxd/eslint-opinionated 0.17.0 → 0.18.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()`. |
@@ -1660,6 +1662,57 @@ por indirección tampoco escapa. Solo aplica a clases con `@Controller`
1660
1662
  (configurable con `controllerDecoratorNames` para decoradores propios); los
1661
1663
  services retornan Result con orgullo — ese es el dominio.
1662
1664
 
1665
+ ### `skapxd/no-anonymous-condition`
1666
+
1667
+ La hermana de `no-else`: esa nombra los **caminos**, esta nombra la
1668
+ **pregunta**. Un `if` cuya condición es un cómputo evalúa un valor anónimo
1669
+ cuyo significado vive solo en la cabeza de quien lo escribió; la regla exige
1670
+ bautizarlo (el refactor "introduce explaining variable" de Fowler, como ley):
1671
+
1672
+ ```ts
1673
+ if (matchesAnyGlob(filename, options.allowFilePatterns)) { ... } // ❌ ¿qué significa que matchee?
1674
+
1675
+ const esArchivoExento = matchesAnyGlob(filename, options.allowFilePatterns);
1676
+ if (esArchivoExento) { ... } // ✅ la decisión se lee como prosa
1677
+ ```
1678
+
1679
+ Lo **ya nombrado** no se extrae (la lista blanca — extraerlo sería ceremonia
1680
+ sin información):
1681
+
1682
+ - Variables y sus negaciones: `isReady`, `!isReady`, `!!isReady`.
1683
+ - Accesos a propiedad hasta `maxMemberDepth` saltos (contando puntos desde
1684
+ la base como nivel 0: `result.ok` → 1, `options.rules.flag` → 2; default
1685
+ `2`) y sus negaciones — incluido el encadenamiento opcional (`config?.flag`).
1686
+ - Comparaciones contra literal booleano o nullish (`x.ok === false`,
1687
+ `x == null`, `x !== undefined`): la escritura explícita de la
1688
+ afirmación/negación/presencia. Cubre las formas oficiales del guard de
1689
+ Result, que `result-error-requires-cause/handling` necesitan ver intactas.
1690
+ - **Type guards demostrados por la firma** (`allowTypePredicates`, default
1691
+ `true`): `if (isFunctionNode(x))` pasa cuando la firma declara
1692
+ `x is FunctionNode` — el type-checker lo demuestra (evidencia, no
1693
+ convención de nombre: una `isX(...)` que devuelve `boolean` a secas sí se
1694
+ extrae). Requiere type info; sin parser services no hay evidencia y toda
1695
+ llamada exige nombre. `Result.isErr(x)` pasa por esta vía: es un type
1696
+ predicate real.
1697
+
1698
+ Lo que **sí dispara**: llamadas, comparaciones (`a.length <= b.max`,
1699
+ `status === "ready"`), combinaciones `&&`/`||` y aritmética
1700
+ (`if (total % 2)`). La extracción directa a `const` conserva el narrowing
1701
+ (TS 4.4+, aliased conditions).
1702
+
1703
+ **Opt-in deliberado: no está en ningún preset.** La calibración contra 4
1704
+ proyectos reales (2026-06-12) midió 473/95/308 hallazgos en tres backends
1705
+ NestJS en producción y 44 en un front pequeño — señal genuina en la muestra
1706
+ revisada, pero un orden de magnitud más invasiva que cualquier regla de las
1707
+ bases. Actívala por proyecto (o por carpeta, estilo ola 3 del playbook de
1708
+ adopción):
1709
+
1710
+ ```js
1711
+ rules: {
1712
+ "skapxd/no-anonymous-condition": "error",
1713
+ }
1714
+ ```
1715
+
1663
1716
  ### `skapxd/no-deep-relative-imports`
1664
1717
 
1665
1718
  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 {
@@ -4086,6 +4237,7 @@ var rules = {
4086
4237
  "nest-no-swagger-in-controllers": nestNoSwaggerInControllers,
4087
4238
  "nest-requires-swagger-plugin": nestRequiresSwaggerPlugin,
4088
4239
  "nest-validation-pipe-config": nestValidationPipeConfig,
4240
+ "no-anonymous-condition": noAnonymousCondition,
4089
4241
  "no-deep-relative-imports": noDeepRelativeImports,
4090
4242
  "no-default-export": noDefaultExport,
4091
4243
  "no-else": noElse,
@@ -4116,4 +4268,4 @@ var rules = {
4116
4268
  export {
4117
4269
  rules
4118
4270
  };
4119
- //# sourceMappingURL=chunk-PD77UDUY.mjs.map
4271
+ //# sourceMappingURL=chunk-77A4SFGD.mjs.map