Skip to content

ci: add extended-lint as a cargo xtask instead of a Python script - #6

Closed
jordigilh wants to merge 1 commit into
praxis-proxy:mainfrom
jordigilh:feat/extended-lint
Closed

ci: add extended-lint as a cargo xtask instead of a Python script#6
jordigilh wants to merge 1 commit into
praxis-proxy:mainfrom
jordigilh:feat/extended-lint

Conversation

@jordigilh

@jordigilh jordigilh commented Aug 18, 2026

Copy link
Copy Markdown

Summary

  • Adds cargo xtask lint-extended, a diff-scoped heuristic checker for low-effort-code patterns clippy/rustfmt can't catch structurally: leftover TODO/FIXME/XXX/HACK markers and commented-out code (blocking), plus narrating "what the code does" comments, diff-local literal repetition, weak/generic identifier names, and new clippy suppressions (warnings, non-blocking).
  • Converts forge into a minimal 2-member Cargo workspace (existing forge binary + new xtask dev-tooling binary) rather than a standalone Python script, so the check is built, clippy'd, and tested with the same toolchain as the rest of the repo.
  • Wires it into the existing extended-lint CI job (now cargo xtask lint-extended instead of python3 scripts/extended-lint.py), and adds --workspace to the clippy/test/doc jobs and --all to fmt so the new xtask member is covered by every other CI job too.

Fixes #5

Why a cargo xtask workspace member instead of a Python script or an in-crate binary

This repo is Rust-native end to end (clippy, rustfmt, cargo test, cargo doc all gate CI); a Python script sat outside all of that — no compiler or lint coverage of its own, a second interpreter/toolchain dependency in CI, and no access to the crate's own regex/anyhow-style tooling conveniences.

Two Rust-native shapes were possible:

  1. A minimal 2-member workspace (chosen): add [workspace] members = ["xtask"] to the existing root Cargo.toml (which keeps its own [package] table, so forge becomes the workspace's root package automatically — no need to relocate any existing src/). xtask is a separate package with its own Cargo.toml, anyhow/regex dependencies, and lint config (via lints.workspace = true against a promoted [workspace.lints] table).
  2. An additional [[bin]] target inside the existing forge crate.

(1) is the lighter-weight, more idiomatic fit and is what this PR implements, for two concrete reasons:

  • Dependency isolation. xtask needs anyhow and regex; neither is a forge runtime dependency. A same-crate [[bin]] would force those into forge's own [dependencies], and thus into the praxis-forge release binary's build graph, unless carefully feature-gated. A separate workspace member keeps them scoped to the dev-tooling binary only.
  • This is the canonical cargo xtask pattern (the convention popularized by matklad and used by e.g. rust-analyzer): a workspace member invoked via a .cargo/config.toml alias (cargo xtask <task>cargo run --locked --package xtask --), so future dev-tooling tasks have an obvious, low-ceremony home instead of accreting more one-off [[bin]] targets or scripts.

The workspace conversion itself is not invasive: the root package keeps its Cargo.toml, src/ layout, and single Cargo.lock exactly as before; the only structural change is one added [workspace] table and promoting the existing [lints.*] tables to [workspace.lints.*] (with lints.workspace = true on both members) so xtask is held to the same ~90-lint deny bar as forge itself.

Design notes

  • Diff-scoped only: it never relitigates pre-existing code, only lines added/changed vs. the diff base (origin/main by default, EXTENDED_LINT_BASE env override, or auto-detected origin/$GITHUB_BASE_REF in a PR run) — ported faithfully from the original design.
  • Two severities: BLOCK fails the job (TODO markers, commented-out code); WARN is printed for human review but does not fail (narrating comments, repeated literals without a named constant, weak identifiers, new #[allow(clippy::...)]/#[expect(clippy::...)] suppressions).
  • xtask/src/lint_extended.rs excludes itself from its own diff scan (via a git diff pathspec exclusion): its doc comments and unit tests legitimately quote the very marker words and comment-like syntax the heuristics look for (e.g. documenting the TODO check requires writing the word "TODO"). The original Python script never hit this because *.rs never matched a .py file — porting to Rust makes the tool a scan target of itself, so the exclusion had to become explicit.
  • All ~90 of the repo's deny-level lints pass on the new code, including missing_docs_in_private_items, unwrap_used/expect_used (regex compilation uses a single documented #[expect(clippy::unwrap_used, reason = "...")] helper for the handful of compile-time-constant patterns), and print_stdout/print_stderr (explicitly #[expect]-annotated at the two reporting call sites, following the same pattern already used in src/main.rs's report_error).

Test plan

  • cargo clippy --locked --workspace --all-targets --features test-support -- -D warnings — clean.
  • cargo fmt --all --check — clean.
  • cargo test --locked --workspace --features test-support — all 273 existing forge tests plus 4 new xtask unit tests pass.
  • cargo doc --locked --workspace --no-deps (with RUSTDOCFLAGS=-D warnings) — clean.
  • Sanity-tested end to end: ran cargo xtask lint-extended origin/main against this PR's own diff (clean, no blocking findings), then added a throwaway // TODO: ... line to src/lib.rs, confirmed cargo xtask lint-extended correctly reported it as a BLOCKING finding and exited non-zero, then discarded the throwaway change before pushing (not included in the diff).
  • CI will exercise the new extended-lint job (and the now---workspace-scoped clippy/test/doc jobs) on this PR itself.

Python doesn't fit a Rust-native codebase: it needs a separate
interpreter/toolchain in CI, gets no compiler or clippy coverage of its
own, and can't share the repo's regex/anyhow dependencies. Convert forge
into a minimal two-member workspace (the existing `forge` binary plus a
new `xtask` dev-tooling crate) and reimplement the diff-scoped heuristic
checks as `cargo xtask lint-extended`, wired into the same CI job.

The repo's shared lint deny-list (unsafe_code, missing_docs_in_private_items,
unwrap_used, and ~90 others) moves to `[workspace.lints]` so both members
inherit it via `lints.workspace = true`, keeping xtask held to the same
bar as the rest of the codebase. `xtask/src/lint_extended.rs` excludes
itself from its own diff scan: its doc comments and tests legitimately
quote the marker words and comment-like syntax the heuristics look for,
which the standalone Python script never triggered on since `*.rs` never
matched a `.py` file.

Fixes praxis-proxy#5

Signed-off-by: Jordi Gil <jgil@redhat.com>
@jordigilh jordigilh changed the title ci: add extended-lint diff-scoped heuristic checks ci: add extended-lint as a cargo xtask instead of a Python script Aug 18, 2026
@jordigilh

Copy link
Copy Markdown
Author

Closing this out without merging.

After digging into what actually exists in the Rust ecosystem for this class of check, most of the "extended lint" heuristics either have no reliable off-the-shelf equivalent in any language's tooling (commented-out-code detection, "narrating" comment detection), or only partial overlap that does not justify maintaining bespoke regex/diff-scoped Rust across six repos (e.g. TODO markers are trivially a one-line git diff | rg in CI rather than a dedicated tool; new-clippy-suppression tracking is already better covered by denying clippy::allow_attributes outright rather than diff-scoping it).

Decision: we do not want to own and maintain custom heuristic lint logic per-repo. If this capability is worth having, the better path is contributing it upstream to a maintained linter (clippy itself, or a dylint/ast-grep rule set) rather than a bespoke xtask subcommand we would need to keep in sync across the org. Not pursuing this further for now.

@jordigilh jordigilh closed this Aug 18, 2026
@jordigilh

Copy link
Copy Markdown
Author

Follow-up on the reasoning above, now that all six ports are done: implementing this surfaced a concrete illustration of exactly the maintenance burden we did not want to sign up for. Every one of the six independent Rust ports had to hand-write a workaround for the same problem -- the linter's own source (its regex definitions, doc comments, and test fixtures) legitimately contains the words TODO/FIXME/etc. as text, so without a manual self-exclusion pathspec the tool blocks on itself. That is not a one-time fix; any future contributor touching this logic (or writing a similar comment-content check) will hit the same trap and need to know to work around it by hand -- exactly the kind of "requires follow-up before it can be safely enabled" friction that is not simple to apply, and part of why we are not carrying this ourselves.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add extended-lint: diff-scoped heuristic checks for low-effort code patterns

1 participant