Toast
Contributor

Linting & Formatting

The static analysis toolchain — what each tool does, where it's configured, and how to work with it.

Toast uses six static analysis tools, each owning one concern. They all run as part of pnpm check and are enforced by git hooks and CI. This page explains what each tool does, when you'll encounter it, and how to work with it.

Why so many tools?

The goal is early, fast feedback that's easy to maintain.

Most of Toast's code is written by AI agents. Agents read terminal output, act on errors, and ignore everything else. That shapes how we configure the toolchain:

  • Every rule is an error. Warnings fill up agent context windows with noise they won't act on. If a pattern is worth linting for, it's worth blocking on. If it's not worth blocking on, remove the rule.
  • Feedback comes from the terminal, not from PR review. The tools run in seconds (Oxlint and Oxfmt are Rust-native), so agents get lint feedback on every commit — not three hours later in a review comment they may not see.
  • Rules are cheap to add. When a pattern keeps showing up in PRs, add a rule. The custom Toast Oxlint plugin and ast-grep rules are straightforward to write — AI can generate them from a description of the problem. The toolchain should keep growing.
  • The codebase keeps improving. Each rule is a ratchet. Once a bad pattern is eliminated, the rule prevents it from returning. Over time the linter does the work that reviewers used to do.

Which tool does what?

I need to...ToolWhy this one
Enforce syntax, style, and per-file correctnessOxlintFast Rust linter with TypeScript support and a custom Toast plugin for project-specific rules
Format code consistentlyOxfmtRust-based formatter for JS/TS/JSON/MD/YAML/CSS/HTML — runs in milliseconds
Format SQL filesPrettierNarrowly scoped to *.sql via prettier-plugin-sql
Detect unused exports, dead files, unused depsknipCross-module visibility that per-file linters can't provide
Enforce architecture and import boundariesdependency-cruiserOperates on the full import graph — apps can't import from other apps, routes can't skip the service layer
Enforce structural code patternsast-grepAST-level pattern matching for dependency injection and architectural rules

Config files

Each tool has exactly one config file at the repo root. These are the source of truth for what's enforced — not this page.

ToolConfigNotes
Oxlint.oxlintrc.jsonBuilt-in rules + custom Toast plugin at tools/oxlint-plugin-toast/plugin.mjs
Oxfmt.oxfmtrc.jsonFormatting options (single quotes, trailing commas, 100 char width)
Prettier.prettierrcSQL-only — prettier-plugin-sql with PostgreSQL dialect
knipknip.jsonWorkspace-level entry points, project files, and ignore patterns
dependency-cruiser.dependency-cruiser.cjsforbidden and allowed rule arrays
ast-grepsgconfig.ymlPoints to rule files in tools/ast-grep-rules/*.yml

Running the tools

pnpm lint            # Oxlint check
pnpm lint:fix        # Oxlint auto-fix
pnpm format          # Oxfmt + Prettier (SQL) check
pnpm format:fix      # Oxfmt + Prettier (SQL) auto-fix
pnpm check:analysis  # Static analysis only: dependency-cruiser, knip, ast-grep
pnpm check           # Everything: lint, format, analysis, typecheck, test, build (with auto-fix)
pnpm check:strict    # Same checks, no auto-fix — this is what the pre-push hook runs

The full pipeline runs in this order:

  1. Lint — Oxlint with the custom Toast plugin
  2. Format — Oxfmt for code, Prettier for SQL
  3. Static analysis (parallelised) — dependency-cruiser, knip, ast-grep
  4. Typecheck — TypeScript in strict mode
  5. Test — Vitest with 100% coverage enforcement
  6. Build — Turbo-orchestrated build

Oxlint in detail

Oxlint is the primary linter. It replaces ESLint entirely — there is no ESLint config in this repo.

The config in .oxlintrc.json starts with all categories disabled and opts in to specific rules. This means every rule is a deliberate choice, not something inherited from a preset.

Plugins enabled: oxc, typescript, unicorn, import, react, vitest, jsx-a11y

Every rule is an error. If a pattern is worth linting for, it blocks CI. There are no warning-level rules — warnings generate noise that agents ignore while filling up their context windows. If a rule isn't worth blocking on, remove it instead of downgrading to a warning.

Overrides tailor rules to different parts of the codebase. Test files relax noConsole and useAwait. Scripts allow noConsole for CLI output. The packages/ui directory is excluded entirely (third-party adapted code).

The custom Toast plugin

The file tools/oxlint-plugin-toast/plugin.mjs adds project-specific rules that enforce Toast conventions. These are things no upstream linter knows about — like requiring requirePermission() middleware on API routes, preventing process.env access outside config modules, or ensuring Zustand stores use selectors.

To see the full list of custom rules, read the plugin file directly. Each rule has a comment explaining what it catches and why.

Dependency-cruiser in detail

Dependency-cruiser enforces architectural boundaries by analysing the import graph. If you violate a boundary, you get a clear error like no-routes-to-repositories: routes/foo.ts → repositories/bar.ts. The key boundaries:

  • Packages can't import from apps — shared code flows downward only
  • Apps can't import from each other — admin ↛ api, api ↛ admin, docs ↛ either
  • Routes can't import repositories or @toast/db directly — go through the service layer
  • No circular runtime dependencies — type-only cycles are allowed

knip in detail

knip detects dead code across the entire monorepo: unused exports, unused dependencies in package.json, files that nothing imports. Each workspace has tailored entry points so knip knows what's "in use" — route files for the API, page components for the admin app, and so on.

ast-grep in detail

ast-grep runs six custom rules that enforce dependency injection conventions from ADR-014. These catch singleton patterns like getDb(), getAuth(), or direct eventBus imports that should be injected via factory function parameters instead.

All ast-grep rules are errors — they fail the build like everything else. When adding a new ast-grep rule to track a migration in progress, fix all existing violations first so the rule can start at zero.

Adding a new rule

  1. Pick the right tool using the table at the top of this page.
  2. Add the rule to the config file. The config is the source of truth — don't create a separate document describing the rule.
  3. Run pnpm check to confirm the rule fires where expected and doesn't false-positive elsewhere.
  4. If you're adding an Oxlint guard rule, add it to tools/oxlint-plugin-toast/plugin.mjs and enable it in .oxlintrc.json with an explicit severity.
  5. If you're adding an ast-grep rule, create a new YAML file in tools/ast-grep-rules/ and test with sg scan --rule tools/ast-grep-rules/your-rule.yml.

Editor setup

Install the Oxc VS Code extension for real-time lint and format feedback. Errors and warnings show as squiggly underlines — fix them as you go.

Git hooks

Three hooks enforce quality at different points:

HookWhat it runsWhen
pre-commitlint-staged (Oxlint + Oxfmt on staged files)Every commit
commit-msgcommitlint (conventional commit format)Every commit
pre-pushpnpm check:strict (the full quality gate)Every push

Never pass --no-verify to skip these hooks. If a hook fails, fix the underlying issue.

On this page