Skip to main content
Back to blog

Critiq team

  • rules
  • guides

Linting vs rule-based code review: what each layer catches

Compare linting vs rule-based code review: style and syntax from ESLint and friends, security and correctness from inspectable Critiq rules.

Linting vs rule-based code review: what each layer catches content

Most teams already run linters in CI. ESLint, RuboCop, golangci-lint, and Ruff catch style drift, unused imports, and a slice of bug patterns before merge. That is valuable, but it is not the same job as rule-based code review for security, correctness, and change-risk findings that need stable IDs and severities your merge policy can reference.

This post compares linting vs rule-based code review: what linters optimize for, what a catalog like @critiq/rules adds on top, and how to run both without duplicate noise. The OSS CLI at https://critiq.dev/products/oss complements linters; it does not replace them.

What linters are built to do

Linters enforce conventions and fast syntactic checks close to the parser. They excel at formatting-adjacent rules, import order, obvious dead code, and language-specific footguns configured in eslint.config.js or pyproject.toml. Output is usually line/column diagnostics without a cross-repo catalog of security mappings.

  • Style and consistency: quotes, spacing, naming conventions
  • Parser-level bugs: unreachable code, unused variables, missing awaits in some setups
  • Framework plugins: React hooks rules, import boundaries in monorepos
  • Fast feedback in the editor via language server integration

Linters are local and deterministic when pinned. They are weaker when you need a finding that cites CWE-89, explains SQL interpolation across ORM layers, or applies the same security bar across TypeScript, Python, and Go in one PR.

What rule-based code review adds

Rule-based code review in Critiq means every finding ties to an inspectable rule file: a stable id such as py.security.sql-interpolation, a severity, remediation text, and optional OWASP or CWE metadata. The same rule runs locally, in CI, and in critiq-action inline comments. You can read, fork, or disable a rule by id instead of guessing which eslint-plugin rule fired.

Critiq rules span security (hardcoded secrets, SSRF patterns, weak crypto), correctness (floating promises, race-prone concurrency), performance (N+1 awaits), and quality signals linters often skip because they need cross-statement facts or framework-aware adapters.

npx @critiq/cli check .
critiq check --format json src/api/

JSON output includes ruleId, severity, location, and message fields your CI can gate on. That is the contract merge automation expects: named rules, not paraphrased prose.

Linting vs rule-based review in practice

Think in layers, not either/or. Linters keep day-to-day style cheap. Rule-based review catches classes of bugs and security issues that need catalog semantics, multi-language parity, and evidence you can attach to a PR thread with a rule link.

  • ESLint no-unused-vars vs Critiq correctness rules on unhandled promises and change-risk tests
  • Bandit or semgrep-style one-offs vs a single @critiq/rules catalog versioned with your CLI
  • Prettier for format vs Critiq for "this literal is a signing key" with CWE-798 metadata
  • Linter autofix on save vs critiq check before push and critiq-action on pull_request

Run both without duplicate noise

Order jobs so fast linters fail first. Run critiq check on the same paths your PR touches; in GitHub Actions, critiq-action diff-scopes to the PR patch so you do not re-litigate unchanged packages in a monorepo. Use .critiq/config.yaml to ignore generated paths linters already exclude.

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - run: npm run lint

  critiq:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0
      - uses: critiq-dev/critiq-action@v1
        with:
          fail-on-severity: high

Wire Critiq on pull requests using https://critiq.dev/integrations/github-actions so reviewers see rule IDs on the diff. When a Critiq finding overlaps a linter warning, prefer the rule-backed comment for merge policy and track linter cleanup separately.

When to extend linters vs add rules

Extend linters for team-specific style and import boundaries you want autofixed in the editor. Add or fork Critiq rules when you need security or correctness checks with severities, documentation links, and the same behavior in every language adapter you ship.

Custom rules live under .critiq/rules/ and validate with critiq rules validate. That path is for org policy encoded as YAML rules, not for replacing ESLint in TypeScript-only repos.

critiq rules validate .critiq/rules/
critiq rules test .critiq/rules/

Next steps

  • First local scan, /blog/your-first-critiq-check
  • TypeScript security examples, /blog/common-typescript-security-findings
  • OSS CLI and catalog, https://critiq.dev/products/oss
  • Rule reference docs, https://docs.critiq.dev/rules

Keep linters for speed and ergonomics. Add rule-based code review when findings need names, severities, and evidence your team can trust at merge time. The combination is stronger than treating one tool as the whole review story.