This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
rm-comments is a Rust CLI that strips comments from source files using tree-sitter
parsing (not regex). It ships three ways: standalone CLI, a Claude Code plugin skill
(skills/rm-comments/SKILL.md), and a Zed editor task. See llms.md for the
agent-facing usage contract and README.md for user docs.
cargo build --release # binary at target/release/rm-comments
cargo test # all tests
cargo test --test golden # one test file (golden|policy|edge|cli)
cargo test golden_fixtures # one test by nameCI (.github/workflows/ci.yml) just runs cargo test on PRs to main / pushes to dev.
Three source files, ~840 lines:
src/lib.rs— the engine.strip_comments()andlist_comments()are the public API;main.rsand tests both go through them.src/languages.rs— theLANGUAGESregistry: extension → grammar → comment node kinds. This is the only file you touch to add a language.src/main.rs— arg parsing, stdin/file/stdout plumbing,--listJSON emission (hand-rolled, no serde), atomic writes, andinstall-zed-task. Directory support lives here too:process_one()handles a single file andrun_dir()walks a directory (via theignorecrate, honoring.gitignore+ skipping hidden dirs), callingprocess_oneper supported file.--sincealso lives here:since_ranges()shells out togit(no new dependency) to build a path → line-ranges map, whichprocess_onefolds into a per-fileOptions.lines. The library is untouched by directory and--sincemode.
- Parse with tree-sitter. If the tree has any error node, bail without writing —
unparseable files are never modified (
collect_comment_ranges). - Walk the tree, collect byte ranges of nodes whose kind is in the language's
comment_kinds. The line-1#!shebang is explicitly excluded even if the grammar tokenizes it as a comment. - Filter ranges by policy. The default keeps doc comments, directives, task markers, and
--keep-matched comments — each has akeep_*flag defaulting on (--strip-doc-comments/--strip-directives/--strip-markersopt out);--linesscopes by range.--applyreplaces the whole policy with an exact id set. - Mask the removed bytes and call
rebuild()— the single source of truth for whitespace cleanup (drop full-comment lines incl. newline, trim gaps before trailing comments, collapse blank-line runs around removals to at most one). If you're changing how output whitespace looks, it's here and nowhere else.
CRLF is normalized to LF for processing and restored at the end; trailing-newline presence is preserved. Empty result-set → returns the original string byte-for-byte.
- Never modify a file that doesn't parse cleanly or has an unknown extension.
- Idempotent: stripping twice == stripping once (asserted in
golden.rs). - Safety-first defaults: doc comments, directives (
DIRECTIVE_PREFIXES), and task markers (MARKER_PREFIXES: TODO/FIXME/HACK/XXX/BUG) are all kept by default — a bare run removes only plain narration. Each opts out via--strip-doc-comments/--strip-directives/--strip-markers. Markers match as a whole leading token (word boundary), so// todosand// buggyare not kept. --listids are positional in current file content; they're invalid after any edit.--applyignores all keep policies.--listhonors--lines/--since, but filters after ids are assigned, so a scoped listing's ids still work with--apply.--sincediffs merge-base → working tree, never → HEAD:--linesand ids address the file on disk, so anything else silently mis-scopes a dirty tree. An empty range set means "skip this file", whichOptions.linescan't express (empty = no scoping there), soprocess_onebails out explicitly instead.- Exit codes:
0ok /1only for--checkwhen changes would be made /2error. On any error the file is left untouched.
cargo add tree-sitter-<lang>- Add one
Langentry toLANGUAGESinsrc/languages.rs. Getcomment_kindsfrom the grammar'snode-types.json— most use"comment"; Rust/Java useline_comment+block_comment, CSS addsjs_comment, JS/TS addhtml_comment. If you add a new multi-kind shape, also extendkind_of()inlib.rs. - Add
tests/fixtures/<name>/input.<ext>+expected.<ext>. The golden test iterates the registry and fails if the fixture pair is missing, so this step is mandatory.
golden.rs— one fixture pair per registered language; strips input → expectsexpected, then re-strips to prove idempotency.policy.rs— directives,--keep,--lines, and the list/apply contract.edge.rs— parser-grounding cases (comment-like text in strings/regexes/f-strings).cli.rs— spawns the built binary: flags, exit codes, atomic-write / never-corrupt guarantees.