Why this case study matters
Deno has a Rust core + JavaScript/TypeScript tooling layout:
~75 Cargo workspace members under cli/, ext/, libs/, runtime/,
tests/, plus a ~3,000-LoC orchestrator at tools/lint.js and a
sibling tools/format.js that drive every CI gate. The orchestrator
runs 9 logical structural checks end-to-end (alongside clippy,
dprint, dlint — the lint engines, which alint shells out to via
command: rather than competing with).
About 45% of the structural surface maps directly to declarative
alint rules — copyright headers per language, top-level-files
allowlist, clippy.toml-per-crate pairing, workflow-generator pairing,
dprint/dlint config shape, file-size guard, .gitattributes LF
enforcement. Roughly 30% needs new alint primitives (the AST-aware
checks and the per-file violation-count baseline). The remaining
25% is out of alint’s scope by design.
Headline catch
Replacing 11 of tools/lint.js’s structural sub-checks plus the
bundled OSS / Rust / Node / GHA / Cargo-workspace baselines with one
declarative config + one alint check invocation in CI is the win:
fewer moving parts, one place to look when CI breaks, and much easier
for forks to keep their hygiene rules consistent with upstream Deno.
The cleanest cross-language pairing in this repo is the
workflow-generator pair: every .github/workflows/*.ts generator
has a paired .generated.yml checked in. The case-study config
captures it explicitly via alint’s pair rule.
The clippy.toml-per-crate pattern is a second high-leverage shape:
every ext/* and libs/* Cargo workspace crate ships a clippy.toml
with a known list of ~30 banned std::fs::* / std::path::Path::* /
url::Url::* methods. The structural piece (file exists per crate,
contains certain strings) is in alint today via for_each_dir +
when_iter. The content-completeness check (“contains every entry
of a 30-method list”) collapses to one rule per entry today, and
becomes one rule once the v0.10 disallowed_methods_in_file
primitive ships.
Where alint earns its keep here
Two themes from this case study are load-bearing for alint’s broader positioning:
-
The “language-AST query” boundary is real and worth documenting up front. Deno hits it twice: the Rust short-flag enforcement in
cli/args/flags.rs(parses Rust to find every.short('X')call) and the TypeScript JSDoc-tag check intools/jsdoc_checker.js(walks a TypeScript AST with ts-morph). alint’s deliberate non-goal: AST analysis. The right messaging is “alint validates structure; if you need to reach into the language AST, keep your existing tool and wrap it via acommandrule. The boundary is deliberate.” -
The baselined-drift primitive (
lintNodePolyfillDenoApis) is the single most-load-bearing missing rule kind for projects mid-migration. It runsdeno lintwith a custom plugin againstext/node/polyfills/**/*.ts, counts the violations, compares per-file count against anEXPECTED_VIOLATIONSbaseline (drift up = error, drift down = error with message “update the baseline”). Same shape shows up for restricted-imports in Kubernetes and Python type-coverage in many other migration efforts. Worth a dedicated v0.10+ design pass: aviolation_baselinerule kind that wraps a child command, parses output, diffs against a per-file snapshot.
Deno also surfaces three cross-cutting v0.10 candidates that have
firmed up to ship-target:
*_path_contains (third confirmation across helm + deno + bazel),
the monorepo/cargo-workspace member-discovery refinement (deno’s
ext/* + libs/* + runtime/* + cli/* layout doesn’t fit the
hardcoded crates/* selector), and the dir_only_contains
subdir-checking flag for the top-level allowlist.
Future story angles
- The “AST boundary” pattern as a reusable framing for any repo with custom validation scripts that mix structural checks with language-AST work. Deno is the sharpest illustration today.
- The
violation_baselineship narrative once that primitive lands — Deno’slintNodePolyfillDenoApisis the cleanest motivating case in the corpus.