code-quality
Code Quality Enforcement
Canonical configuration patterns for JavaScript/TypeScript and Python code quality tooling. Auto-detect project type, generate config files, and integrate with CI.
When to Use This Skill
- Setting up linting and formatting for a new project
- Reviewing or upgrading existing code quality configs
- Generating ESLint, Prettier, Ruff, or mypy configuration files
- Adding lint/format steps to GitHub Actions CI
- Diagnosing why a linter isn't catching issues (wrong config, missing rules)
- Migrating from legacy tooling (TSLint → ESLint, Black/flake8/isort → Ruff)
Auto-Detection
Identify the project stack from these markers before generating configs:
| Marker file | Stack | Tools to configure |
|---|---|---|
package.json |
JavaScript/TypeScript | ESLint, Prettier |
tsconfig.json |
TypeScript (additional) | TypeScript strict mode, tsc --noEmit |
pyproject.toml |
Python | Ruff, mypy |
setup.py or setup.cfg |
Python (legacy) | Ruff, mypy |
Both package.json + pyproject.toml |
Full-stack | All of the above |
When auto-detecting, check for existing configs first (.eslintrc*, eslint.config.*, .prettierrc*, [tool.ruff] in pyproject.toml) — upgrade rather than overwrite.
JavaScript/TypeScript Stack
ESLint (Flat Config — v9+)
Use eslint.config.mjs (flat config). Legacy .eslintrc.* is deprecated as of ESLint v9.
// eslint.config.mjs
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import prettierConfig from "eslint-config-prettier";
export default tseslint.config(
js.configs.recommended,
tseslint.configs.strictTypeChecked,
tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-import-type-side-effects": "error",
},
},
prettierConfig,
{
ignores: ["node_modules/", "dist/", ".next/", "coverage/"],
},
);
For JS-only projects (no TypeScript), drop tseslint and use:
// eslint.config.mjs
import js from "@eslint/js";
export default [
js.configs.recommended,
{
rules: {
"no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
},
},
{ ignores: ["node_modules/", "dist/"] },
];
Next.js projects — use eslint-config-next with FlatCompat (the Next.js config still uses legacy format internally):
// eslint.config.mjs
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import prettierConfig from "eslint-config-prettier";
import { FlatCompat } from "@eslint/eslintrc";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const compat = new FlatCompat({ baseDirectory: __dirname });
export default tseslint.config(
js.configs.recommended,
tseslint.configs.strictTypeChecked,
...compat.extends("next/core-web-vitals"),
// ... rules, prettierConfig, ignores
);
Install: npm install -D eslint @eslint/js typescript-eslint eslint-config-prettier prettier eslint-config-next @eslint/eslintrc
Prettier
{
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "always"
}
.prettierignore (uses .gitignore syntax):
node_modules/
dist/
.next/
coverage/
pnpm-lock.yaml
To avoid ESLint/Prettier conflicts, install eslint-config-prettier and add it as the last config entry before ignores (see the ESLint template above where prettierConfig is already included).
TypeScript Strict Mode
Key tsconfig.json strict settings — enable all of these:
{
"compilerOptions": {
"strict": true, // Enables all strict flags below
"noUncheckedIndexedAccess": true, // Array/object index returns T | undefined
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"exactOptionalPropertyTypes": true
}
}
strict: true enables: strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitAny, noImplicitThis, alwaysStrict, useUnknownInCatchVariables.
Python Stack
Ruff (Linting + Formatting)
Ruff replaces flake8, isort, Black, pyupgrade, and more. Single tool for both linting and formatting.
# pyproject.toml
[tool.ruff]
target-version = "py312"
line-length = 120
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"SIM", # flake8-simplify
"TCH", # flake8-type-checking
"RUF", # ruff-specific rules
]
ignore = [
"E501", # line length (handled by formatter)
]
[tool.ruff.lint.isort]
known-first-party = ["your_package"] # REPLACE with your actual package name
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
Commands: ruff check --fix . (lint), ruff format . (format).
Migration from Black/flake8/isort: Ruff is a drop-in replacement. Remove legacy configs (.flake8, .isort.cfg, pyproject.toml [tool.black]) and consolidate into [tool.ruff].
mypy (Strict Type Checking)
# pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = true # enables disallow_untyped_defs, no_implicit_optional, warn_return_any, etc.
warn_unused_ignores = true # included in strict since mypy 1.0 — explicit here for clarity
For incremental adoption on existing codebases, start with per-module overrides:
[tool.mypy]
python_version = "3.12"
warn_return_any = true
[[tool.mypy.overrides]]
module = "your_package.core.*"
strict = true
[[tool.mypy.overrides]]
module = "your_package.legacy.*"
ignore_errors = true
Black (Legacy)
Prefer Ruff for new projects. Black is only needed for teams already using it. Ruff's formatter is Black-compatible.
# pyproject.toml (if still using Black)
[tool.black]
line-length = 120
target-version = ["py312"]
CI Integration
GitHub Actions — JS/TS
Pin actions to commit SHAs to prevent supply chain attacks. Add permissions: contents: read for least privilege.
# .github/workflows/lint.yml
name: Lint
on: [push, pull_request]
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: ".nvmrc" # or node-version: "22"
cache: "npm"
- run: npm ci
- run: npx --no-install eslint --max-warnings 0 .
- run: npx --no-install prettier --check .
- run: if [ -f tsconfig.json ]; then npx --no-install tsc --noEmit; fi
GitHub Actions — Python
# .github/workflows/lint.yml
name: Lint
on: [push, pull_request]
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2f # v5.3.0
with:
python-version: "3.12"
cache: "pip"
- run: pip install -r requirements-dev.txt # ruff, mypy pinned
- run: ruff check .
- run: ruff format --check .
- run: mypy --ignore-missing-imports . # remove flag once type stubs are installed
GitHub Actions — Full-Stack (Combined)
name: Code Quality
on: [push, pull_request]
permissions:
contents: read
jobs:
lint-js:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: ".nvmrc"
cache: "npm"
- run: npm ci
- run: npx --no-install eslint --max-warnings 0 .
- run: npx --no-install prettier --check .
- run: if [ -f tsconfig.json ]; then npx --no-install tsc --noEmit; fi
lint-python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2f # v5.3.0
with:
python-version: "3.12"
cache: "pip"
- run: pip install -r requirements-dev.txt
- run: ruff check .
- run: ruff format --check .
- run: mypy --ignore-missing-imports .
Config Generation Workflow
When generating configs for a project, follow this order:
- Detect stack — check marker files (see Auto-Detection table)
- Check existing configs — don't overwrite; upgrade or merge
- Install dependencies — add dev dependencies for the detected stack
- Generate config files — use the templates above, customizing
known-first-party,ignores, and project-specific rules - Add CI workflow — generate the appropriate GitHub Actions lint job
- Add npm/pyproject scripts — e.g.,
"lint": "eslint .","format": "prettier --write ." - Verify — run the linters to confirm zero false positives on the existing codebase
Package Installation Commands
JS/TS (npm):
npm install -D eslint @eslint/js typescript-eslint eslint-config-prettier prettier # versions pinned in package.json
Python (pip/uv):
pip install ruff mypy # pin in requirements-dev.txt: ruff==0.11.x mypy==1.15.x