refactor!: tighten lexer API surface and relocate WordSpan to ast#70
Merged
Conversation
The `lexer` module has been `pub(crate)` at the crate root since day one,
so nothing inside it was externally reachable — yet many items carried
misleading `pub` markers. This PR makes visibility declarations match
actual reachability and tightens the public AST surface.
Changes:
- `lexer/heredoc.rs`: `PendingHereDoc` -> `pub(crate)` with `pub(crate)`
fields. Dropped unused `quoted` field (and the corresponding param from
`queue_heredoc`). Dropped the `#[allow(dead_code)]` attribute.
- `lexer/word_builder.rs`: `WordBuilder`, `QuotingContext`, `WordSpanKind`
-> `pub(crate)`. Per-field `pub(crate)`. Methods on `WordBuilder` -> `pub(crate)`.
- `lexer/mod.rs`: `heredoc` / `word_builder` modules, `Lexer`, `LexerContext`,
`LexerMode`, `LexerCheckpoint`, `PendingHereDoc` re-export, and all
`impl Lexer` methods -> `pub(crate)` / `pub(super)`. Added
`#![allow(clippy::redundant_pub_crate)]` since pedantic clippy warns
about `pub(crate)` inside an already-crate-private module, but the
markers are intentional for visibility-boundary documentation.
- `ast.rs`: `WordSpan` struct definition moved here from
`lexer/word_builder.rs`. Kept `pub` so the public
`NodeKind::Word { spans: Vec<WordSpan> }` variant satisfies Rust's
`private_in_public` check. Fields are `pub(crate)` so external consumers
see the type as opaque — they can pattern-match
`NodeKind::Word { value, parts, .. }` (the pre-existing practice) and
the type is not nameable via any `pub` path outside the crate.
- `lexer/word_builder.rs`: `pub(crate) use crate::ast::WordSpan` re-export
for internal convenience so existing `crate::lexer::word_builder::WordSpan`
imports keep working.
BREAKING CHANGE: `WordSpan` is now defined in `rable::ast` rather than
`rable::lexer::word_builder`. The lexer module was already crate-private
at the public boundary, so external consumers never had access to it via
any stable path. The only observable change in the `pub` API is the
location of `WordSpan` in documentation. `WordSpan` itself is still
publicly reachable through `rable::ast::WordSpan`, but its fields are
crate-private and cannot be read by external code.
Verification:
- cargo fmt && cargo clippy --all-targets -- -D warnings: clean
- cargo test: 252 passed (197 unit + 54 integration + 1 tree-sitter)
- cargo test --test integration oracle_: 12 passed
- PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 cargo check --features python: clean
- Downstream ../rippy (rable = "0.1.13"): 1332/1332 tests pass with patch
- Downstream ../tokf (rable = "0.1.15"): 145/147 tests pass with patch;
the 2 failures are pre-existing TLS-cert env issues that reproduce
against origin/main rable — unrelated to this change.
Completes the v0.2.0 refactoring cycle (#61). Release-please will observe
this commit's `refactor!:` prefix + `BREAKING CHANGE:` footer and emit a
release PR bumping `Cargo.toml` and `pyproject.toml` from 0.1.15 to 0.2.0
(per `.release-please-config.json` with `bump-minor-pre-major: true`).
Closes #60
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mpecan
pushed a commit
that referenced
this pull request
Apr 19, 2026
🤖 I have created a release *beep* *boop* --- ## [0.2.0](rable-v0.1.15...rable-v0.2.0) (2026-04-18) ### ⚠ BREAKING CHANGES * tighten lexer API surface and relocate WordSpan to ast ([#70](#70)) ### Bug Fixes * **format:** align cmdsub reformatter with bash canonical form ([#49](#49)) ([c7a4411](c7a4411)) * **lexer:** accept sloppy heredoc terminator in cmdsub mode ([#50](#50)) ([40f394f](40f394f)) * **lexer:** backticks opaque when content is invalid ([#71](#71)) ([e72166f](e72166f)), closes [#38](#38) * **lexer:** disable reserved-word recognition after assignment words ([#44](#44)) ([42e1fc0](42e1fc0)) * **lexer:** stop treating ]] and unbalanced [...] as special outside conditionals ([#45](#45)) ([4bf5a5c](4bf5a5c)) * **parser:** fall back from (( … )) arith to nested subshells ([#48](#48)) ([1437f00](1437f00)) ### Code Refactoring * **format:** introduce Formatter struct ([#65](#65)) ([d965a8f](d965a8f)) * **lexer:** drop Result<Token> wrapper from operator readers ([#62](#62)) ([d52a841](d52a841)) * **lexer:** split read_word_token into classify + advance + dispatch helpers ([#63](#63)) ([3ba09f5](3ba09f5)) * **parser:** extract fill_heredoc_contents visitor helpers ([#68](#68)) ([40e6165](40e6165)) * **parser:** extract helpers from three oversize parsers ([#69](#69)) ([25d0762](25d0762)) * **sexp:** dispatch NodeKind Display to per-category helpers ([#66](#66)) ([44b0330](44b0330)) * **sexp:** table-drive ANSI-C escape dispatch ([#67](#67)) ([91a5267](91a5267)) * tighten lexer API surface and relocate WordSpan to ast ([#70](#70)) ([5171d01](5171d01)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: repository-butler[bot] <166800726+repository-butler[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Final PR of the v0.2.0 refactoring cycle (#61). Tightens the library's API surface so visibility declarations match actual reachability, relocates
WordSpanfromlexer/word_builder.rstosrc/ast.rs, and removes the deadPendingHereDoc.quotedfield.Visibility tightening
The
lexermodule is alreadypub(crate) mod lexerat lib.rs, so nothing inside was externally reachable. This PR makes the markers match:PendingHereDoc→pub(crate)withpub(crate)fields; dropped unusedquotedfield and the#[allow(dead_code)]attribute.WordBuilder,QuotingContext,WordSpanKind→pub(crate).Lexer,LexerContext,LexerMode,LexerCheckpoint→pub(crate)struct +pub(crate)fields; allimpl Lexermethods →pub(crate).heredoc/word_buildersubmodules →pub(crate)/pub(super).#![allow(clippy::redundant_pub_crate)]tosrc/lexer/mod.rsandsrc/lexer/word_builder.rs— pedantic clippy warns aboutpub(crate)inside an already-pub(crate)module, but the markers are intentional for documenting the visibility boundary.WordSpan relocation
WordSpannow lives insrc/ast.rs(alongsideSpan,Node,NodeKind). Rationale: Rust'sprivate_in_publiccheck requiresWordSpanto havepubvisibility since it's a field type of the publicNodeKind::Word { spans: Vec<WordSpan> }variant. The struct ispub structwithpub(crate)fields — external consumers see the type as opaque (can pattern-match and hold references but cannot read fields or construct instances).WordSpanKindandQuotingContextstay inlexer/word_builder.rsaspub(crate)(they're only used in thepub(crate)fields ofWordSpan).A
pub(crate) use crate::ast::WordSpanre-export inword_builder.rspreserves existingcrate::lexer::word_builder::WordSpanimports across the crate.Semver
This PR uses a
refactor!:commit prefix and aBREAKING CHANGE:trailer. Per.release-please-config.jsonwithbump-minor-pre-major: true, release-please will emit a release PR bumpingCargo.tomlandpyproject.tomlfrom0.1.15→0.2.0when this lands on main. No manual version edit in this PR.External observable change: the canonical path for
WordSpanis nowrable::ast::WordSpan. The lexer module was already crate-private at the public boundary, so no consumer had access via any stable path.rippy(usesNode/NodeKind/ListItem/ListOperatoronly) andtokf(usesast::*only) are unaffected.Test plan
cargo fmtcargo clippy --all-targets -- -D warnings— no warningscargo test— 252 passed (197 unit + 54 integration + 1 tree-sitter)cargo test --test integration oracle_— 12 passedPYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 cargo check --features python— clean../rippy(rable 0.1.13) — 1332/1332 tests pass with local rable patch../tokf(rable 0.1.15) — 145/147 tests pass with local rable patch; the 2 failures are pre-existingrustls-native-certsTLS env issues that reproduce againstorigin/mainrable and are unrelated to this changeStack
Part of the v0.2.0 refactoring cycle (#61). This is PR 10 of 10 — final PR of the cycle. After merge, release-please will emit the 0.2.0 release PR.
Closes #60
🤖 Generated with Claude Code