linters

Linters y Herramientas de Calidad de Código

Introducción a Linters

Los linters son herramientas esenciales en el desarrollo moderno que analizan el código para identificar problemas potenciales, errores de sintaxis, y violaciones de convenciones de estilo.

ESLint

Configuración y Uso

ESLint es el linter más popular para JavaScript/TypeScript, altamente configurable y extensible.

Configuración básica:

// .eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
    node: true
  },
  extends: [
    'eslint:recommended',
    '@typescript-eslint/recommended'
  ],
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint'],
  rules: {
    'no-unused-vars': 'error',
    'prefer-const': 'warn',
    'no-console': 'warn'
  }
};

Scripts de package.json:

{
  "scripts": {
    "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
    "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
  }
}

Características Principales

  • Plugins para frameworks específicos (React, Vue, Angular)
  • Parsers para TypeScript y nuevas características de JavaScript
  • Reglas configurables con diferentes niveles de severidad
  • Integración con editores de código y sistemas de CI/CD
  • Auto-fix para problemas corregibles automáticamente

Biome

Herramienta Unificada

Biome es una herramienta moderna que combina funcionalidades de linter y formateador en una sola herramienta escrita en Rust.

Configuración básica (biome.json):

{
  "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
  "vcs": {
    "enabled": true,
    "clientKind": "git",
    "useIgnoreFile": true
  },
  "files": {
    "ignoreUnknown": false,
    "ignore": []
  },
  "formatter": {
    "enabled": true,
    "indentStyle": "tab"
  },
  "organizeImports": {
    "enabled": true
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true
    }
  },
  "javascript": {
    "formatter": {
      "quoteStyle": "single"
    }
  }
}

Ventajas de Biome

  • Rendimiento extremadamente rápido gracias a estar escrito en Rust
  • Baterías incluidas - no requiere configuración compleja
  • Zero-config para empezar rápidamente
  • Compatibilidad con reglas de ESLint y Prettier
  • Menos dependencias en el proyecto

Formatters

Prettier vs Biome

Prettier:

// .prettierrc
{
  "semi": true,
  "trailingComma": "es5",
  "singleQuote": true,
  "printWidth": 80,
  "tabWidth": 2,
  "useTabs": false
}

Comparación:

  • Prettier: Especializado en formateo, ampliamente adoptado
  • Biome: Enfoque unificado (linter + formatter), mejor rendimiento
  • Configuración: Prettier requiere configuración separada, Biome es más integrado

Oxc (Oxidation Compiler)

Compilador JavaScript en Rust

Oxc es un conjunto de herramientas de alto rendimiento para JavaScript/TypeScript escrito en Rust.

Características:

  • Parser ultra-rápido para JavaScript/TypeScript
  • Linter con reglas personalizables
  • Transformer para operaciones de código
  • Minimizer para optimización de bundles
  • Compatibilidad con el ecosistema JavaScript existente

Uso potencial:

// Integración futura con herramientas existentes
const { OxcCompiler } = require('@oxc/compiler');

Husky para Git Hooks

Automatización de Pre-commits

Husky permite ejecutar scripts automáticamente en hooks de Git, asegurando calidad antes de los commits.

Configuración:

// package.json
{
  "scripts": {
    "prepare": "husky install",
    "pre-commit": "lint-staged"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,html,css,scss}": [
      "prettier --write"
    ]
  }
}

Setup:

# Instalación y configuración
npm install husky lint-staged --save-dev
npx husky init
npm run prepare

Flujo de Trabajo Integrado

Configuración Completa

Ejemplo de setup completo:

// package.json
{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
    "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix",
    "format": "prettier --write .",
    "format:check": "prettier --check .",
    "prepare": "husky install"
  },
  "devDependencies": {
    "eslint": "^8.0.0",
    "prettier": "^3.0.0",
    "husky": "^8.0.0",
    "lint-staged": "^13.0.0"
  }
}

Beneficios del Setup

  • Calidad consistente del código en todo el equipo
  • Detección temprana de problemas y errores
  • Formato uniforme automáticamente
  • Integración continua sin sorpresas
  • Mejor experiencia de desarrollo

Estrategias de Implementación

Para Proyectos Nuevos

  1. Biome como herramienta unificada para máxima velocidad
  2. Husky para hooks de pre-commit automáticos
  3. Configuración zero-config cuando sea posible

Para Proyectos Existentes

  1. ESLint + Prettier para compatibilidad
  2. Migración gradual a herramientas más modernas
  3. Configuración incremental de reglas

Reglas Recomendadas

  • Errores críticos: Bloqueantes para el build
  • Advertencias: Para mejoras de código
  • Auto-fix: Siempre que sea posible
  • Exclusiones estratégicas: Para código legacy o casos específicos

Linters - Temas Avanzados y Configuraciones Especializadas

Configuraciones Específicas por Framework

React ESLint Configuration

// .eslintrc.react.js
module.exports = {
  env: {
    browser: true,
    es2021: true
  },
  extends: [
    'eslint:recommended',
    '@typescript-eslint/recommended',
    'plugin:react-hooks/recommended',
    'plugin:jsx-a11y/recommended'
  ],
  plugins: ['react', 'react-hooks', 'jsx-a11y'],
  rules: {
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',
    'react/jsx-uses-react': 'off',
    'react/react-in-jsx-scope': 'off',
    'jsx-a11y/anchor-is-valid': 'warn'
  },
  settings: {
    react: {
      version: 'detect'
    }
  }
};

Vue ESLint Setup

// .eslintrc.vue.js
module.exports = {
  extends: [
    'plugin:vue/vue3-essential',
    '@vue/typescript/recommended'
  ],
  rules: {
    'vue/multi-word-component-names': 'warn',
    'vue/no-unused-vars': 'error',
    'vue/require-default-prop': 'error'
  },
  parser: 'vue-eslint-parser',
  parserOptions: {
    parser: '@typescript-eslint/parser',
    ecmaVersion: 2020
  }
};

Configuraciones para Entornos Específicos

Node.js/Backend Configuration

// .eslintrc.node.js
module.exports = {
  env: {
    node: true,
    es2021: true
  },
  extends: [
    'eslint:recommended',
    '@typescript-eslint/recommended'
  ],
  rules: {
    'no-console': 'off',
    'no-process-exit': 'error',
    'global-require': 'warn',
    'no-sync': 'warn',
    'handle-callback-err': 'error'
  },
  overrides: [
    {
      files: ['**/*.test.js', '**/*.spec.js'],
      env: { jest: true }
    }
  ]
};

Configuración para Testing (Jest)

// .eslintrc.jest.js
module.exports = {
  env: {
    'jest/globals': true
  },
  plugins: ['jest'],
  extends: ['plugin:jest/recommended'],
  rules: {
    'jest/no-disabled-tests': 'warn',
    'jest/no-focused-tests': 'error',
    'jest/no-identical-title': 'error',
    'jest/prefer-to-have-length': 'warn',
    'jest/valid-expect': 'error'
  }
};

Reglas Personalizadas y Plugins

Custom ESLint Rules

// eslint-custom-rules.js
module.exports = {
  rules: {
    'no-relative-imports': {
      create: function(context) {
        return {
          ImportDeclaration(node) {
            if (node.source.value.startsWith('.')) {
              context.report({
                node,
                message: 'Use absolute imports instead of relative imports'
              });
            }
          }
        };
      }
    }
  }
};

Plugin de Seguridad

// eslint-security.config.js
module.exports = {
  plugins: ['security'],
  extends: ['plugin:security/recommended'],
  rules: {
    'security/detect-object-injection': 'warn',
    'security/detect-eval-with-expression': 'error',
    'security/detect-non-literal-require': 'error'
  }
};

Configuraciones de Performance

ESLint con Caching

// .eslintrc con cache
{
  "cache": true,
  "cacheLocation": "node_modules/.cache/eslint/",
  "cacheStrategy": "metadata",
  "rules": {
    "no-unused-vars": "error"
  }
}

Biome Performance Optimization

// biome.json con optimizaciones
{
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "performance": {
        "noAccumulatingSpread": "error",
        "noDelete": "warn"
      }
    }
  },
  "files": {
    "maxSize": 2097152
  }
}

Integración con Editores

Configuración VS Code

// .vscode/settings.json
{
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact"
  ],
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
    "source.fixAll.biome": "explicit"
  },
  "eslint.workingDirectories": [
    {"mode": "auto"}
  ],
  "biome.lspBin": "./node_modules/@biomejs/biome/bin/biome"
}

Configuración WebStorm

// .idea/inspectionProfiles/Project_Default.xml
<component name="InspectionProjectProfileManager">
  <profile version="1.0">
    <option name="myName" value="Project Default" />
    <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
    <inspection_tool class="TypeScript" enabled="false" level="ERROR" enabled_by_default="false" />
  </profile>
</component>

CI/CD Integration

GitHub Actions Setup

# .github/workflows/lint.yml
name: Lint and Test
on: [push, pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run type-check
      - run: npm run test
  biome:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: biomejs/setup-biome@v1
        with:
          version: latest
      - run: biome ci .

GitLab CI Configuration

# .gitlab-ci.yml
stages:
  - lint
  - test

eslint:
  stage: lint
  image: node:18
  script:
    - npm ci
    - npm run lint

biome:
  stage: lint
  image: node:18
  script:
    - npm ci
    - npx @biomejs/biome ci .

Monorepo Configurations

ESLint en Monorepo

// packages/eslint-config-custom/index.js
module.exports = {
  extends: ['eslint:recommended'],
  rules: {
    'import/no-relative-parent-imports': 'error'
  },
  overrides: [
    {
      files: ['**/src/**/*.{js,ts}'],
      rules: {
        'no-console': 'error'
      }
    }
  ]
};

Biome en Monorepo

// biome.json para monorepo
{
  "files": {
    "ignore": ["**/node_modules/**", "**/dist/**", "**/build/**"]
  },
  "formatter": {
    "enabled": true
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true
    }
  }
}

Reglas de Import/Export

ESLint Import Rules

// eslint-imports.config.js
module.exports = {
  plugins: ['import'],
  rules: {
    'import/order': [
      'error',
      {
        'groups': [
          'builtin',
          'external',
          'internal',
          'parent',
          'sibling',
          'index'
        ],
        'newlines-between': 'always'
      }
    ],
    'import/no-unresolved': 'error',
    'import/no-duplicates': 'error',
    'import/no-cycle': 'error'
  }
};

Análisis de Bundle y Performance

ESLint Plugin para Bundle Size

// eslint-bundle.config.js
module.exports = {
  plugins: ['bundle-size'],
  rules: {
    'bundle-size/no-large-dependencies': [
      'error',
      {
        maxSize: '100KB',
        exclude: ['react', 'react-dom']
      }
    ]
  }
};

Configuraciones para Legacy Code

ESLint para Código Legacy

// eslint-legacy.config.js
module.exports = {
  rules: {
    'no-var': 'off',
    'prefer-const': 'off',
    'prefer-arrow-callback': 'off'
  },
  overrides: [
    {
      files: ['legacy/**/*.js'],
      rules: {
        'no-unused-vars': 'warn',
        'no-console': 'off'
      }
    }
  ]
};

Internacionalización y Localización

ESLint para i18n

// eslint-i18n.config.js
module.exports = {
  plugins: ['i18n'],
  rules: {
    'i18n/no-hardcoded-strings': 'error',
    'i18n/text-restrictions': 'warn'
  }
};

Accessibility Linting

ESLint para Accesibilidad

// eslint-a11y.config.js
module.exports = {
  plugins: ['jsx-a11y'],
  rules: {
    'jsx-a11y/alt-text': 'error',
    'jsx-a11y/anchor-is-valid': 'error',
    'jsx-a11y/label-has-associated-control': 'error'
  }
};

Security-Focused Linting

Reglas de Seguridad

// eslint-security-advanced.config.js
module.exports = {
  plugins: ['security', 'no-unsanitized'],
  rules: {
    'security/detect-possible-timing-attacks': 'error',
    'security/detect-buffer-noassert': 'error',
    'no-unsanitized/method': 'error',
    'no-unsanitized/property': 'error'
  }
};

Linters - Configuraciones Avanzadas y Casos de Uso Especializados

Configuraciones para Microfrontends

ESLint Config Multi-proyecto

// eslint.config.microfrontends.js
export default [
  // Configuración base para todos los proyectos
  {
    files: ["**/*.{js,ts,jsx,tsx}"],
    rules: {
      "no-console": "warn",
      "prefer-const": "error"
    }
  },
  // Configuración específica para shared libraries
  {
    files: ["packages/shared/**/*"],
    rules: {
      "no-restricted-imports": ["error", {
        patterns: [{
          group: ["../*", "./*"],
          message: "Use imports absolutos en shared libraries"
        }]
      }]
    }
  },
  // Configuración para aplicaciones host
  {
    files: ["apps/**/*"],
    rules: {
      "import/no-relative-packages": "off"
    }
  }
];

Linters para GraphQL

ESLint para GraphQL Schemas

// eslint-graphql.config.js
module.exports = {
  plugins: ['@graphql-eslint'],
  rules: {
    '@graphql-eslint/known-type-names': 'error',
    '@graphql-eslint/no-deprecated': 'warn',
    '@graphql-eslint/unique-field-definition-names': 'error'
  },
  overrides: [
    {
      files: ['*.graphql'],
      parser: '@graphql-eslint/eslint-plugin',
      rules: {
        '@graphql-eslint/alphabetize': ['error', {
          fields: ['ObjectTypeDefinition', 'InterfaceTypeDefinition']
        }]
      }
    }
  ]
};

Linters para CSS/SCSS

Stylelint Configuration

// .stylelintrc.js
module.exports = {
  extends: [
    'stylelint-config-standard',
    'stylelint-config-prettier'
  ],
  rules: {
    'color-no-invalid-hex': true,
    'font-family-no-duplicate-names': true,
    'declaration-block-no-duplicate-properties': true,
    'selector-class-pattern': null,
    'custom-property-pattern': null
  },
  ignoreFiles: [
    '**/node_modules/**',
    '**/dist/**',
    '**/coverage/**'
  ]
};

Stylelint con SCSS

// .stylelintrc.scss.js
module.exports = {
  extends: [
    'stylelint-config-standard-scss',
    'stylelint-config-recommended-scss'
  ],
  rules: {
    'scss/dollar-variable-pattern': null,
    'scss/selector-no-redundant-nesting-selector': true,
    'scss/no-global-function-names': true
  }
};

Linters para Markdown

ESLint para Markdown

// eslint-markdown.config.js
module.exports = {
  plugins: ['markdown'],
  overrides: [
    {
      files: ['**/*.md'],
      processor: 'markdown/markdown',
      rules: {
        'no-console': 'off',
        'no-unused-vars': 'off'
      }
    },
    {
      files: ['**/*.md/*.{js,ts}'],
      rules: {
        'no-console': 'off',
        'prefer-const': 'warn'
      }
    }
  ]
};

Markdownlint Configuration

// .markdownlint.json
{
  "default": true,
  "MD013": false,
  "MD024": false,
  "MD025": false,
  "line-length": false,
  "no-inline-html": false,
  "first-line-heading": false,
  "single-title": false
}

Linters para Docker

Hadolint Configuration

# .hadolint.yaml
failure-threshold: warning
ignored:
  - DL3008
  - DL3018
  - DL3059
override:
  error:
    - DL3003
    - DL3006
  warning:
    - DL3015
    - DL3025
trustedRegistries:
  - docker.io
  - registry.hub.docker.com

Linters para GitHub Actions

Actionlint Setup

# actionlint configuration example
# .github/workflows/ci.yml con linting integrado
name: CI
on: [push, pull_request]
jobs:
  lint-workflows:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: rhysd/actionlint@v1
        with:
          config-file: .actionlint.yaml

Linters para JSON/YAML

ESLint para JSON

// eslint-json.config.js
module.exports = {
  plugins: ['json'],
  overrides: [
    {
      files: ['*.json', '*.json5'],
      parser: 'jsonc-eslint-parser',
      rules: {
        'jsonc/sort-keys': 'error'
      }
    }
  ]
};

YAML Lint Configuration

# .yamllint.yaml
extends: default
rules:
  braces: enable
  brackets: enable
  colons: enable
  commas: enable
  comments: disable
  comments-indentation: disable
  document-start: disable
  empty-lines: enable
  hyphens: enable
  indentation:
    level: error
    indent-sequences: consistent
  key-duplicates: enable
  line-length: disable
  new-line-at-end-of-file: enable
  new-lines: enable
  trailing-spaces: enable

Linters para Performance

ESLint Performance Rules

// eslint-performance.config.js
module.exports = {
  rules: {
    'no-multiple-empty-lines': ['error', { max: 2 }],
    'max-depth': ['error', 4],
    'complexity': ['error', 10],
    'max-params': ['error', 4],
    'no-nested-ternary': 'error',
    'prefer-template': 'error',
    'no-loop-func': 'warn'
  }
};

Linters para TypeScript Avanzado

ESLint TypeScript Estricto

// eslint-typescript-strict.config.js
module.exports = {
  extends: ['@typescript-eslint/recommended-requiring-type-checking'],
  parserOptions: {
    project: './tsconfig.json',
    tsconfigRootDir: __dirname
  },
  rules: {
    '@typescript-eslint/no-explicit-any': 'error',
    '@typescript-eslint/no-unsafe-assignment': 'error',
    '@typescript-eslint/no-unsafe-call': 'error',
    '@typescript-eslint/no-unsafe-member-access': 'error',
    '@typescript-eslint/no-unsafe-return': 'error',
    '@typescript-eslint/strict-boolean-expressions': 'error'
  }
};

Linters para Code Quality Metrics

ESLint para Métricas de Código

// eslint-metrics.config.js
module.exports = {
  rules: {
    'max-lines': ['warn', 300],
    'max-lines-per-function': ['warn', 50],
    'max-statements': ['warn', 15],
    'max-nested-callbacks': ['error', 3],
    'cognitive-complexity': ['warn', 15]
  }
};

Linters para Commit Messages

Commitlint Configuration

// commitlint.config.js
module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      ['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'revert']
    ],
    'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case']],
    'header-max-length': [2, 'always', 72]
  }
};

Linters para SEO y Meta Tags

ESLint para SEO

// eslint-seo.config.js
module.exports = {
  plugins: ['jsx-seo'],
  rules: {
    'jsx-seo/no-missing-meta-tags': 'error',
    'jsx-seo/no-duplicate-meta-tags': 'error',
    'jsx-seo/require-meta-description': 'warn',
    'jsx-seo/require-open-graph-tags': 'warn'
  }
};

Linters para Internationalization

ESLint para i18n

// eslint-i18n-advanced.config.js
module.exports = {
  plugins: ['i18n-ally'],
  rules: {
    'i18n-ally/key-style': ['error', 'nested'],
    'i18n-ally/no-duplicate-keys': 'error',
    'i18n-ally/no-missing-keys': 'warn',
    'i18n-ally/no-unused-keys': 'error'
  },
  settings: {
    'i18n-ally': {
      localeDir: './locales',
      sourceLanguage: 'en',
      supportedLngs: ['en', 'es', 'fr']
    }
  }
};

Linters para Web Components

ESLint para Custom Elements

// eslint-web-components.config.js
module.exports = {
  plugins: ['wc'],
  rules: {
    'wc/guard-super-call': 'error',
    'wc/no-closed-shadow-root': 'error',
    'wc/no-constructor-attributes': 'error',
    'wc/no-invalid-element-name': 'error',
    'wc/no-self-class': 'error'
  }
};

Linters para Deno

Deno Lint Configuration

// deno.json
{
  "lint": {
    "rules": {
      "tags": ["recommended"],
      "include": ["ban-untagged-todo"],
      "exclude": ["no-console"]
    }
  },
  "fmt": {
    "useTabs": false,
    "lineWidth": 100,
    "indentWidth": 2,
    "singleQuote": true
  }
}

Linters para Rust (WASM Projects)

Clippy Configuration

# .cargo/config.toml
[build]
rustflags = ["-D", "warnings"]

[clippy]
all-targets = true
all-features = true

Linters para Python (Full Stack)

Flake8 Configuration

# .flake8
[flake8]
max-line-length = 100
exclude = .git,__pycache__,build,dist
ignore = E203, W503
per-file-ignores =
    __init__.py: F401
    tests/*: S101

Linters para Shell Scripts

ShellCheck Configuration

# .shellcheckrc
disable=SC1090,SC1091
enable=add-default-case,avoid-nullary-conditions,check-unassigned-uppercase
exclude=SC2154
shell=bash

Linters para SQL

SQLFluff Configuration

# .sqlfluff
[sqlfluff]
dialect = postgres
exclude_rules = L011,L031

[sqlfluff:rules]
comma_style = trailing
allow_scalar = True
single_table_references = consistent

Linters para Terraform

TFLint Configuration

# .tflint.hcl
config {
  module = true
  force = false
}

rule "terraform_deprecated_interpolation" {
  enabled = true
}

rule "terraform_unused_declarations" {
  enabled = true
}

rule "terraform_comment_syntax" {
  enabled = true
}

Linters para OpenAPI/Swagger

Spectral Configuration

# .spectral.yaml
extends:
  - spectral:oas
rules:
  info-contact: error
  operation-operationId: warn
  path-parameters: error
  no-script-tags-in-markdown: off

Linters para Jupyter Notebooks

nbQA Configuration

# .nbqa.ini
[nbqa]
addopts = --nbqa-mutate
mypy = --ignore-missing-imports
flake8 = --extend-ignore=E402,F401

Linters para Mobile Development

ESLint para React Native

// eslint-react-native.config.js
module.exports = {
  env: {
    'react-native/react-native': true
  },
  plugins: ['react-native'],
  rules: {
    'react-native/no-unused-styles': 'error',
    'react-native/split-platform-components': 'warn',
    'react-native/no-inline-styles': 'warn',
    'react-native/no-color-literals': 'warn'
  }
};

Linters para WebAssembly

ESLint para WASM

// eslint-wasm.config.js
module.exports = {
  rules: {
    'no-restricted-syntax': [
      'error',
      {
        selector: 'CallExpression[callee.name="WebAssembly.instantiate"]',
        message: 'Use async WebAssembly.instantiateStreaming for better performance'
      }
    ]
  }
};