Skip to content

refactor!: tighten lexer API surface and relocate WordSpan to ast#70

Merged
mpecan merged 1 commit into
mainfrom
feat/60-api-tightening
Apr 18, 2026
Merged

refactor!: tighten lexer API surface and relocate WordSpan to ast#70
mpecan merged 1 commit into
mainfrom
feat/60-api-tightening

Conversation

@mpecan
Copy link
Copy Markdown
Owner

@mpecan mpecan commented Apr 18, 2026

Summary

Final PR of the v0.2.0 refactoring cycle (#61). Tightens the library's API surface so visibility declarations match actual reachability, relocates WordSpan from lexer/word_builder.rs to src/ast.rs, and removes the dead PendingHereDoc.quoted field.

Visibility tightening

The lexer module is already pub(crate) mod lexer at lib.rs, so nothing inside was externally reachable. This PR makes the markers match:

  • PendingHereDocpub(crate) with pub(crate) fields; dropped unused quoted field and the #[allow(dead_code)] attribute.
  • WordBuilder, QuotingContext, WordSpanKindpub(crate).
  • Lexer, LexerContext, LexerMode, LexerCheckpointpub(crate) struct + pub(crate) fields; all impl Lexer methods → pub(crate).
  • heredoc / word_builder submodules → pub(crate) / pub(super).
  • Added #![allow(clippy::redundant_pub_crate)] to src/lexer/mod.rs and src/lexer/word_builder.rs — pedantic clippy warns about pub(crate) inside an already-pub(crate) module, but the markers are intentional for documenting the visibility boundary.

WordSpan relocation

WordSpan now lives in src/ast.rs (alongside Span, Node, NodeKind). Rationale: Rust's private_in_public check requires WordSpan to have pub visibility since it's a field type of the public NodeKind::Word { spans: Vec<WordSpan> } variant. The struct is pub struct with pub(crate) fields — external consumers see the type as opaque (can pattern-match and hold references but cannot read fields or construct instances). WordSpanKind and QuotingContext stay in lexer/word_builder.rs as pub(crate) (they're only used in the pub(crate) fields of WordSpan).

A pub(crate) use crate::ast::WordSpan re-export in word_builder.rs preserves existing crate::lexer::word_builder::WordSpan imports across the crate.

Semver

This PR uses a refactor!: commit prefix and a BREAKING CHANGE: trailer. Per .release-please-config.json with bump-minor-pre-major: true, release-please will emit a release PR bumping Cargo.toml and pyproject.toml from 0.1.150.2.0 when this lands on main. No manual version edit in this PR.

External observable change: the canonical path for WordSpan is now rable::ast::WordSpan. The lexer module was already crate-private at the public boundary, so no consumer had access via any stable path. rippy (uses Node/NodeKind/ListItem/ListOperator only) and tokf (uses ast::* only) are unaffected.

Test plan

  • cargo fmt
  • cargo clippy --all-targets -- -D warnings — no warnings
  • 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 local rable patch
  • Downstream ../tokf (rable 0.1.15) — 145/147 tests pass with local rable patch; the 2 failures are pre-existing rustls-native-certs TLS env issues that reproduce against origin/main rable and are unrelated to this change

Stack

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

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 mpecan merged commit 5171d01 into main Apr 18, 2026
5 checks passed
@mpecan mpecan deleted the feat/60-api-tightening branch April 18, 2026 09:08
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&lt;Token&gt; 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>
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.

PR 10: API tightening + v0.2.0 bump

1 participant