typos
Typos Tool Analysis
Overview
typos is a source-code spell checker written in Rust. It is designed to find and correct misspellings in code and documentation with a very low false-positive rate — it understands programming conventions (identifiers, escape sequences, hex literals) so it rarely flags legitimate tokens. This analysis compares Lintro’s wrapper implementation with the core typos tool and documents the parser-selection decision.
Core Tool Capabilities
- Fast, broad scanning: checks all text files in a tree; binary files are detected and skipped automatically when typos walks the tree itself. Paths named explicitly on the command line bypass both that binary detection and the configured excludes, so the Lintro wrapper compensates (see below).
- Auto-fix:
typos --write-changesapplies corrections in place. - Low false positives: a curated dictionary keyed on common misspellings rather than a full natural-language dictionary.
- Configurable: project config via
typos.toml,.typos.toml, or_typos.toml(custom dictionaries through[default.extend-words], file scope through[files] extend-exclude). - Output formats:
long(default, human),brief,silent,json, andsarif.
Lintro Implementation
- Definition:
lintro/tools/typos/definition.py—can_fix=True,tool_type=ToolType.LINTER,file_patterns=["*"], native configstypos.toml/.typos.toml/_typos.toml. - Check: runs
typos --format json --force-exclude <files>and parses the newline-delimited JSON from stdout only, so a warning on stderr cannot corrupt the report. - Fix: detects issues, runs
typos --format json --force-exclude --write-changes <files>(the same always-on wrapper flags as check, so[files] extend-excludestill applies), then re-checks to report theinitial = fixed + remainingbreakdown expected by Lintro’s fix pipeline. - Explicit-path safeguards: Lintro always passes a resolved file list, and typos
(like ripgrep) skips its ignore rules for paths given as arguments.
--force-excluderestores the project’s[files] extend-exclude, and the plugin additionally skips known binary suffixes and sniffs the first 8 KiB of remaining files for a NUL byte so--write-changesstays away from images, archives, and other binary assets. The sniff is a heuristic for suffix-less binaries, not a guarantee: unusual binary assets should still be listed in[files] extend-exclude. - ARG_MAX batching: with
file_patterns=["*"]a large tree would otherwise expand into a single argv that exceeds the OS limit and fails withE2BIG. Paths are split into budget-sized batches bylintro/tools/core/argv_batching.py(shared with TruffleHog) and the per-batch results are merged. A batch that exits non-zero without a parseable report is tracked separately from the merged findings, so a genuine failure in one batch is never hidden by typos another batch reported. Two signals feed that: typos’ own{"type": "error", ...}records on stdout (which can appear in the same batch as real findings, e.g. one unreadable file among many), and a non-zero exit with nothing parseable at all. - Parser:
lintro/parsers/typos/(parse_typos_report,TyposIssue). Each finding captures the file, line, a 1-based column derived from the reported byte offset, the misspelled word, and its suggested corrections.fixableis set from whether typos offered any correction, andcolumnis a 1-based byte index (typos reports byte offsets and the source line is not available to the parser), left at 0 when typos reported no usable offset. The composed message has the form"<typo>" should be "<correction>", with several corrections comma-separated. When typos offers no corrections at all, the message instead reads"<word>" is disallowed(this happens for words banned through configuration rather than matched against the correction dictionary).
JSON output shape
typos --format json emits one object per finding:
{
"type": "typo",
"path": "README.md",
"line_num": 3,
"byte_offset": 18,
"typo": "<misspelled>",
"corrections": ["<suggested>"]
}
Only type == "typo" records become findings. type == "error" records are surfaced by
_parse_typos_errors and fail the run even when findings were also parsed. The plugin
calls parse_typos_report (the only public parser entry), which pairs both views of the
same stdout so check, fix, --write-changes, and the post-write re-check cannot consume
findings without also seeing diagnostics. The conventional parse_<tool>_output name is
intentionally not public: it would return an empty findings list for a diagnostic-only
stream and look like a clean scan. Informational types (binary_file, file_type) are
debug-logged and dropped. Any other record type is treated as a diagnostic so a future
typos release cannot vanish.
typos 1.49.0’s Message enum also has file and parse, but the default spell-check
walker never emits them (file is --files listing; parse is the identifier/word
dump), so they stay off the allowlist.
A non-zero exit with no parseable findings also fails closed; typos’ findings exit code is 2, and any other non-zero is a runtime failure even if some JSON typos were emitted.
When typos is selected
typos is language-agnostic, so it has no entry in the manifest’s language_map. That
splits selection in two:
- On a no-config first run, tool selection comes from language detection
(
_detection_scoped_tool_namesinlintro/utils/execution/tool_configuration.py), which does not select typos on its own. It enters through the “unmapped tool with a native config” branch — once atypos.toml,.typos.tomlor_typos.tomlexists at a scan root. crate-ci/typos also reads[tool.typos]inpyproject.tomland[package.metadata.typos]/[workspace.metadata.typos]inCargo.toml; those filenames are intentionally omitted fromnative_configsso every Python or Rust project does not auto-enable the plugin. The binary still honors those tables once typos is selected. An empty[tool.lintro]table is not a config (the guard istools is None and config.config_path is None and not config.tools), so this path still applies. - With any resolved lintro config (
.lintro-config.yaml/.yml, a non-empty[tool.lintro]table inpyproject.toml, or an in-memorytools:section), or under--tools all, language scoping is skipped. Typos then runs when the binary is onPATHand it is not filtered byexecution.enabled_toolsortools.typos.enabled: false.lintro init --profile recommendedwrites a language-mapenabled_toolsallowlist that does not include typos. An unscoped config (enabled_tools: []or omitted) will start spell-checking on upgrade.
Explicit selection (--tools typos) works in either case.
Parser choice: native JSON vs shared SARIF
typos can emit SARIF (--format sarif), so per the SARIF ingestion evaluation
(docs/design/sarif-ingestion-evaluation.md, Refs #1066) we assessed whether the shared
SARIF parser would be lossless here. It is not, so Lintro uses a native JSON
parser. What SARIF would drop for typos:
- The structured
typo/correctionsfields. These are the entire point of a spell checker. In SARIF they are not first-class: the word and its fix are only reachable by reverse-parsing the human-readablemessage.markdown/ thefixes[].artifactChanges[].replacements[].insertedContent.text. The native JSON hands them over directly astypoandcorrections. - The message. typos populates
message.markdownonly — it emits nomessage.text. The shared SARIF parser readsmessage.text, so it would yield an empty message for every finding. - Rule identity / doc URLs. typos has no rule IDs and emits no
rules[]array, so the SARIF path contributes nocodeordoc_url— nothing gained over native JSON.
SARIF’s only extra for typos is a richer fix region (start/end line and column), which Lintro does not consume because it re-runs the tool in fix mode rather than applying parsed replacements. Given the enrichment loss on the fields that matter most, the native JSON parser is the higher-fidelity choice.
Configuration in this repository
This repository keeps an unscoped enabled_tools: [] config, so typos runs on
lintro check here. That is not universal: no-config first runs and
lintro init --profile recommended do not select typos (see When typos is
selected). The repo ships a .typos.toml (the project’s spell-checker config,
analogous to .hadolint.yaml and .yamllint). It declares a small set of intentional
project vocabulary and excludes a few test fixtures that deliberately embed non-English
or scrambled text. Every entry is documented inline; it is a curated dictionary, not a
generated suppression baseline.
Installation
cargo install typos-cli # from crates.io
brew install typos-cli # Homebrew
Lintro’s scripts/utils/install-tools.sh installs it automatically (pre-built binary
via cargo-quickinstall, falling back to cargo install typos-cli).