EngineeringCode Quality

Linting Standards

IDE-level enforcement and shared linting configuration across the engineering team.

Overview

Linting is the automated process of analyzing source code for potential errors, style violations, and anti-patterns — before the code is ever run or reviewed by a human. A well-configured linting setup acts as a force-multiplier: it catches entire classes of bugs at the moment of writing, eliminates stylistic debates in code review, and ensures every engineer — regardless of editor or OS — produces consistent output.

This page covers how we configure, share, and enforce linting rules across the team, so "it looks fine to me" never becomes a production incident.


Why It Matters

Consistency at scale. As teams grow, individual preferences diverge. Tabs vs. spaces, trailing commas, import ordering — none of these decisions matter in isolation, but a codebase with ten different styles is hard to read and harder to diff. Linting makes the style decision once, then enforces it automatically forever.

Shift-left on bugs. Many runtime bugs — unused variables, implicit type coercions, unreachable code, missing await — are detectable statically. Catching them in the editor is orders of magnitude cheaper than catching them in QA or production.

Cheaper code review. When automated tooling handles formatting and obvious anti-patterns, reviewers can spend their attention on logic, architecture, and intent — the things only a human can evaluate.

Onboarding speed. A new engineer who clones the repo and opens their editor should get immediate, accurate feedback on whether their code meets team standards — without reading a style guide first.


Standards & Best Practices

The golden rule: lint rules are team decisions, not personal preferences

Never add or remove a rule unilaterally. Lint config changes affect every engineer's workflow. Propose changes in a PR with a clear rationale; the team agrees or disagrees explicitly.

Use the same config everywhere

The lint config lives in the repository root. Engineers, CI, and IDEs all read the same file. If CI uses a different rule set than your editor, you will have a bad time.

Errors vs. warnings

SeverityWhen to useCI behavior
errorRules that indicate a real bug or a violation the team has committed to never shippingFails the build
warnRules in trial period, or style preferences the team is phasing inBuild passes, printed in output
offRules that conflict with the tech stack or are consciously rejectedNot reported

Avoid warning sprawl. If a warning isn't worth fixing, it isn't worth reporting. Either promote it to an error or turn it off.

Prefer shareable configs over ad-hoc rules

Start from a well-maintained shareable config (e.g. eslint-config-airbnb, @typescript-eslint/recommended, eslint-config-next) and override only where necessary. This gives you a solid baseline and makes upgrades easier.

Auto-fix everything that can be auto-fixed

If a rule can be fixed automatically (formatting, import order, trailing commas), configure your editor and CI to fix it — don't just report it. Reporting fixable issues creates noise without value.

Scope rules to where they apply

Not all rules apply to all files. Use override blocks to apply test-specific rules only to test files, relax certain rules in generated code, or enforce stricter rules in security-sensitive paths.

{
  "overrides": [
    {
      "files": ["**/*.test.ts", "**/*.spec.ts"],
      "rules": {
        "@typescript-eslint/no-explicit-any": "off"
      }
    }
  ]
}

How to Implement

Step 1 — Choose your linter

Language / ecosystemRecommended linter
TypeScript / JavaScriptESLint + @typescript-eslint
PythonRuff (replaces Flake8, isort, and parts of pylint)
PHPPHP_CodeSniffer (phpcs) or PHP CS Fixer
Gogolangci-lint
CSS / SCSSStylelint
Markdown / MDXmarkdownlint

Use Prettier (or Ruff's formatter for Python) for formatting. Keep formatting separate from linting — linters catch bugs, formatters enforce style. Running both together eliminates the entire "how should this look?" class of review comments.

Step 2 — Commit your config files

All config files belong in the repository root, committed to version control:

.eslintrc.json        # or eslint.config.js (flat config)
.prettierrc
.stylelintrc.json
ruff.toml
phpcs.xml             # PHP_CodeSniffer ruleset
.php-cs-fixer.php     # PHP CS Fixer config (alternative to phpcs)
.markdownlint.json

Never rely on global installations or per-machine config. If it isn't in the repo, it doesn't exist for someone else's machine or for CI.

Step 3 — Add lint scripts to package.json (or equivalent)

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

Separate lint (report only) from lint:fix (auto-fix). CI should run lint and format:check; developer machines should run lint:fix and format during the save-on-write cycle.

Step 4 — Configure IDE integration

Every engineer should have their editor apply lint fixes and formatting on save. Document this in the repo's README or onboarding guide, and commit shared editor settings where possible:

VS Code — commit .vscode/settings.json:

{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "eslint.validate": ["javascript", "typescript", "typescriptreact"]
}

JetBrains IDEs — enable "ESLint" under Settings → Languages & Frameworks → JavaScript → Code Quality Tools → ESLint, and check "Run eslint --fix on save".

Step 5 — Enforce in CI

Lint must run in CI on every pull request. A PR that doesn't pass lint should not be mergeable.

# GitHub Actions example
- name: Lint
  run: pnpm lint

- name: Format check
  run: pnpm format:check

CI lint runs should be report-only (no --fix). If CI auto-fixes and commits, you lose the paper trail of what changed and why.


Tools & Templates

Minimal ESLint flat config (TypeScript + Next.js)

// eslint.config.js
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import { FlatCompat } from '@eslint/eslintrc';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({ baseDirectory: __dirname });

export default [
  ...compat.extends('next/core-web-vitals', 'next/typescript'),
  {
    rules: {
      '@typescript-eslint/no-unused-vars': 'error',
      '@typescript-eslint/no-explicit-any': 'warn',
      'no-console': ['warn', { allow: ['warn', 'error'] }],
    },
  },
  {
    files: ['**/*.test.ts', '**/*.spec.ts'],
    rules: {
      '@typescript-eslint/no-explicit-any': 'off',
    },
  },
];

Prettier config

{
  "semi": true,
  "singleQuote": false,
  "trailingComma": "all",
  "printWidth": 100,
  "tabWidth": 2
}

Ruff config (Python)

# ruff.toml
line-length = 100
target-version = "py311"

[lint]
select = ["E", "F", "I", "UP", "B", "SIM"]
ignore = ["E501"]  # line length is handled by the formatter

[format]
quote-style = "double"

PHP_CodeSniffer config (phpcs.xml)

<?xml version="1.0"?>
<ruleset name="Project">
  <description>Project coding standard</description>
  <rule ref="PSR12"/>
  <file>src</file>
  <arg name="extensions" value="php"/>
  <arg name="colors"/>
  <arg value="sp"/>
</ruleset>

Run: vendor/bin/phpcs to check, vendor/bin/phpcbf to auto-fix.

.eslintignore / ignore patterns

.next/
node_modules/
dist/
*.generated.ts
coverage/

Common Pitfalls

Ignoring lint locally, relying on CI to catch it. CI feedback cycles are slow. Run lint in your editor and before pushing. If your editor isn't surfacing lint errors on save, that's a setup issue — fix it.

Too many warnings. Warnings that are never acted on become background noise. Every existing warning is tacit permission to add another. Treat your warning count like a bug count: it should trend toward zero.

Different versions of ESLint or plugins across machines. Pin exact versions in package.json using pnpm lockfiles. A rule that errors on one machine and passes on another destroys trust in the tooling.

Disabling rules with inline comments without explanation. // eslint-disable-next-line with no comment is a red flag. If you need to suppress a rule, explain why in the same comment. Unexplained suppressions are technical debt with no context.

Linting but not formatting. Lint and format solve different problems. Teams that lint but don't auto-format still argue about style in code review. Run both.

Adding lint after the codebase is large. The later you add linting, the more violations you inherit. When introducing lint to an existing codebase, use --fix to auto-fix everything possible on day one, then temporarily warn everything that can't be auto-fixed and schedule fixes as part of normal work — don't let the warning count grow.