Skip to content

Languages and engines

Every file is handled by one of four mechanisms (defined in the glossary):

  1. Engines from components — signed downloads a core build fetches per language; linked in on the bundled build. Configured as [engines.<id>]. See Components and offline use for the component list; poly components list names the engines in each.
  2. Host tools — a language’s canonical CLI, used when it is on PATH (rustfmt, gofmt, shellcheck by default; the rest opt-in).
  3. Tree-sitter tier — best-effort formatting for identified languages with nothing better.
  4. Recipes and catalog tools — opt-in [tools.<name>]. See Toolchains, runtimes and recipes.

In the How it runs column, ↓ marks a recipe tool polylint can download as a pinned, checksummed release (macOS and Linux), and RT one that needs a host runtime.

Language or files Engine Lint Format How it runs
JavaScript / TypeScript / JSX / TSX oxc yes yes component web
JSON / JSONC oxc parse diagnostics + formatter yes yes component web
Python ruff internals yes yes component python
Python (type checking) opt-in project check, ty ([engines.python-types] backend = "ty") or pyrefly (backend = "pyrefly"); imports resolve through the detected virtualenv’s site-packages; --only/--skip accept python-types for either, and a backend whose component is disabled or missing is an error, not a silent skip (see Python type checking) opt-in no component python (ty) or pyrefly
JS/TS workspaces (npm, pnpm, yarn, bun) skif (opt-in dependency, manifest, version-policy and dead-code project check; [engines.skif] enabled = true) opt-in no component web
TOML taplo (lint) + oxc_formatter_toml (format; taplo’s formatter when a taplo layout key is set) yes yes component web
Markdown rumdl (lint) + oxc_formatter_markdown (format; rumdl’s fixer when a rumdl rule key is set) yes yes component web
Markdown ADRs (NNNN-*.md) adrlint (opt-in MADR structural rules) opt-in no component web
SQL sqruff yes yes component sql
YAML serde_yaml (libyaml, the grammar PyYAML implements) + oxc_formatter_yaml yes yes component web
CSS / SCSS oxc_formatter_css (format; malva when a malva key is set) + biome (lint) yes yes components web, biome
Less oxc_formatter_css (malva when a malva key is set) no yes component web
HTML / Vue / Svelte / Astro / Angular / templates / XML markup_fmt no yes component web
GraphQL graphql-parser + pretty_graphql (parse-error lint + format) + biome (rule lint) yes yes components misc, biome
HCL / Terraform hcl-edit (lint) + a port of hclwrite.Format (the layout terraform fmt and terragrunt hclfmt produce); opt-in tflint, tofu, terraform yes yes component misc; recipes (tflint ↓, tofu ↓, terraform)
Dockerfile (Dockerfile, Dockerfile.*, Dockerfile-*, *.dockerfile, Containerfile, …) vendored dockerfile-parser, hadolint-style rules; opt-in hadolint yes no component misc; recipe hadolint ↓
.env files (.env, .env.*, *.env) dotenv-analyzer yes no component misc
INI and compatible (.ini, .cfg, .desktop, .npmrc, .editorconfig, .pylintrc, .coveragerc, .flake8, …) rust-ini yes no component misc
Nix alejandra no yes component nix
Ruby (*.rb, Gemfile, Rakefile, *.gemspec, *.rake, *.ru, Podfile, …) rubyfmt no yes component ruby
PHP mago yes yes component php
Shell shfmt (the host shfmt in the bundled build, or with [components] disable = ["sh"]); shellcheck when present (default-on) optional yes component sh (Go sidecar); host shellcheck
Go gofmt when present (default-on), imports grouped as goimports does; tree-sitter fallback otherwise; opt-in golangci-lint opt-in yes host; recipe golangci-lint ↓
Rust rustfmt when present (default-on), tree-sitter fallback otherwise; cargo clippy, cargo-sort, cargo-machete, cargo-deny in the whole-project phase whole-project yes host
Java opt-in google-java-format, tree-sitter fallback otherwise; opt-in checkstyle, palantir-java-format opt-in yes host; recipes checkstyle ↓ RT (JRE), palantir-java-format ↓
Kotlin opt-in ktfmt, tree-sitter fallback otherwise no yes host
Swift opt-in swift-format, tree-sitter fallback otherwise no yes host
Zig opt-in zig fmt, tree-sitter fallback otherwise no yes host
Dart opt-in dart format, tree-sitter fallback otherwise no yes host
Gleam opt-in gleam format, tree-sitter fallback otherwise no yes host
R opt-in styler (via Rscript), tree-sitter fallback otherwise no yes host
Groovy opt-in npm-groovy-lint opt-in opt-in recipe RT (Node)
Protobuf opt-in buf, tree-sitter fallback otherwise opt-in yes recipe buf ↓
Starlark / Bazel (BUILD, *.bzl, WORKSPACE, Tiltfile, …) opt-in buildifier, tree-sitter fallback otherwise opt-in yes recipe buildifier ↓
GitHub Actions workflows opt-in actionlint, gau opt-in no recipes actionlint ↓, gau
Helm charts opt-in helm, helm-template opt-in no recipes ↓
pyproject.toml opt-in pyproject-fmt no opt-in recipe RT (Python)
All text files opt-in typos spell-check ([engines.typos] enabled = true) opt-in no component typos
Any recognized language opt-in uncomment comment removal opt-in no component uncomment
Other identified grammars (C, C++, C#, Elixir, …) tree-sitter generic tier no best effort tree-sitter

Lint in this table is the language’s own engine. The two cross-cutting tiers below add findings on top, including where the column says no.

Unsupported or unknown file types are skipped unless tree-sitter-language-pack can identify them. Some whitespace-sensitive data, template or patch grammars intentionally no-op rather than risk a destructive rewrite. Lock files (Cargo.lock, pnpm-lock.yaml, yarn.lock, …) are never linted or formatted.

Beyond the dedicated engines above, two lint tiers run on every language:

  • The native quality metric engine — file and function length, type length, parameter count, nesting depth, cyclomatic complexity, lazy ignore markers, and test files the runner will not collect. See code quality metrics.
  • A built-in ast-grep rule pack of 26 rules across C#, Elixir, Go, Java, Kotlin, Python, Ruby, Rust and Swift. 13 are on by default; the other 13 author their own severity: off and are one extend_select away.

A pack rule whose findings are concentrated in generated or test code declares those paths in its own YAML, with the standard ast-grep ignores: key — placeholder-implementation excludes **/*_generated.rs, force-cast excludes **/RustBridge/**, not-null-assertion excludes Kotlin test source sets, and unwrap-used excludes test and generated paths. Those are defaults: a [per_file_ignores] entry naming the rule replaces them, and every finding they drop is listed in the run’s suppressed array. See per-file ignores.

Both run alongside the engines in the table, at warning severity — so neither fails an unconfigured CI on its own.

Numbered ADR files (NNNN-*.md) can additionally be checked against the MADR template by the opt-in adrlint backend. It is separate from rumdl on purpose: rumdl judges the Markdown, adrlint judges the decision record’s section structure, front-matter metadata, list style and document-local link anchors. Enable it with [engines.adrlint] enabled = true.

It runs the document rules per file, and the tree-wide rules — numbering, gaps, duplicates and file names (MADR41–MADR44) and the immutability lock (ADR50/ADR51) — as a project check over the ADR log. Link-target existence (MADR12a/MADR12c) and external links (MADR11) depend on the tree or the network, which a content-hash-cached engine cannot model honestly; the adrlint CLI remains the tool for those. See architecture decision records.

oxlint compiles in whole plugins that its default set (unicorn, typescript, oxc) leaves switched off — vitest, jest, jsx-a11y, promise, import, node, vue and others. Rule selection cannot turn one on: a filter resolves only against the rules of plugins that are already enabled, so extend_select = ["vitest/expect-expect"] on its own does nothing at all. Name the plugin instead:

[engines.oxc]
plugins = ["vitest"]

The plugin’s correctness rules come on with it, and poly’s own categories are layered on top, so the rules then behave like any other. An unrecognised plugin name fails the run rather than being ignored — a dropped name would leave you believing a rule set is live over a run that checks nothing of the kind.

.env files autofix with poly lint --fix, and inline # dotenv-linter:off <Check> / # dotenv-linter:on <Check> comments suppress a check for the lines between them — the same directive syntax the standalone dotenv-linter CLI understands.

Rules: DuplicatedKey, EndingBlankLine, ExtraBlankLine, IncorrectDelimiter, KeyWithoutValue, LeadingCharacter, LowercaseKey, QuoteCharacter, SpaceCharacter, SubstitutionKey, TrailingWhitespace, UnorderedKey, ValueWithoutQuotes, SchemaViolation.

rust-ini’s parser discards comments while parsing, so writing its model back out would silently delete every comment in the file. poly never does that — INI formatting stays with the tree-sitter generic tier instead, which preserves comments structurally.

Rules: parse-error (a real syntax error, with the parser’s own line and column), duplicate-key, duplicate-section, key-without-value, inconsistent-separator, trailing-whitespace.

Detection is deliberately narrow. These are never treated as INI:

  • *.conf — most are not INI; nginx, httpd and friends use their own syntax.
  • *.properties — Java’s key=value syntax, not INI’s.
  • .gitconfig — quoted subsections rust-ini cannot parse.
  • systemd units — duplicate keys are legal there.

The generic tier identifies and best-effort formats hundreds of grammars, including first-class detection for C, C++, Elixir, Protobuf and the long tail covered by tree-sitter-language-pack. Java and Kotlin reach it only as the fallback when their opt-in native formatter is absent; both, along with C, C++, C# and Ruby, are languages the cross-cutting code-quality tier lints in full.

For everything else, opt into tools from the embedded mdsf catalog. Entries are PATH-probed and skipped when absent, so enabling one never breaks a run:

[tools.prettier]
enabled = true
files = "**/*.{js,ts}"