Closed
st0012 marked this pull request as ready for review
February 21, 2026 17:58kou approved these changes Mar 1, 2026
st0012 added a commit that referenced this pull request
Mar 5, 2026Convert `ClassModule#comment_location` from an Array of `[comment,
location]` pairs to a Hash of `{ location => [comments] }`.
## Motivation
When a class like `RDoc` is documented across multiple files
(`lib/rdoc.rb`, `lib/rdoc/rubygems_hook.rb`, etc.), each file
contributes a comment. With the old Array of `[comment, location]`
pairs, there was no efficient way to look up or replace comments by
file. This matters for server live-reload (#1620): when a user edits
`lib/rdoc.rb`, we need to clear that file's comment and re-parse — but
preserve comments from other files **in their original order**. The
Array structure made this fragile because removing and re-appending an
entry moved it to the end, changing the order comments appeared on the
rendered page.
The Hash structure solves this: `{ location => [comments] }` allows O(1)
lookup by file, and Ruby hashes preserve insertion order — replacing a
key's value keeps it in position.
A secondary benefit: a class reopened in the same file now preserves all
its comments:
```ruby
# comment1
class A; end
# comment2
class A; end
```
With the old Array, the C parser had a special-case `delete_if` to
deduplicate same-location entries, while Ruby parsers accumulated
duplicates inconsistently. With the new Hash, each location maps to an
array of comments — both comments are preserved and rendered, matching
the cross-file behavior.
## Changes
- `add_comment`: appends to array per location —
`(@comment_location[location] ||= []) << comment`
- `parse`: uses `flat_map` to flatten per-location comment arrays into
Document parts
- `marshal_load` / `merge`: use `group_by(&:file)` to reconstruct arrays
from Documents
- `documented?`, `search_snippet`, `from_module`: updated for new value
shape
- `i18n/text.rb`: handles Hash with array values
- C parser `delete_if` special case removed (hash key naturally
deduplicates by location)
Merged
Adds a built-in HTTP server for previewing documentation while editing source files. Parses all sources on startup, watches for file changes, re-parses only changed files, and auto-refreshes the browser. Server implementation (lib/rdoc/server.rb): - Uses Ruby's built-in TCPServer (no WEBrick or external dependencies) - Persistent Aliki generator instance rendering to strings - Thread-per-connection with Connection: close (no keep-alive) - Background watcher thread polls file mtimes every 1 second - Live reload via inline JS polling /__status endpoint - New --server[=PORT] option (default 4000) and rdoc:server Rake task - Moved RDoc::Servlet to RDoc::RI::Servlet (server mode uses new class) Security: - Binds to 127.0.0.1 only (localhost) - Path traversal protection in asset serving via expand_path containment - Proper HTTP error responses (400, 404, 405, 500) - 5-second read timeout on client sockets Concurrency: - Mutex protects all store mutations, generator refresh, and cache invalidation as a single atomic operation - Thread-safe last_change_time reads for the status endpoint Correctness: - Clears file contributions (methods, constants, comments, etc.) before re-parsing to prevent duplication, without removing shared namespaces - Individual parse_file errors caught so one failure doesn't block others - Store#remove_file recursively cleans nested classes/modules and C vars - Watcher thread uses @running flag with clean shutdown via join
- Embed last_change_time into the live-reload script at render time so the browser's initial timestamp matches the page content. This fixes a race where a change between page generation and the first poll would be silently skipped. - Call clear_file_contributions for removed files (not just changed files) and remove classes/modules from the store when no files contribute to them anymore. This correctly handles reopened classes across multiple files and improves file deletion behavior.
Move `relative_path_for` from a private method on RDoc::Server to a public method on RDoc::RDoc, eliminating the duplication with the inline logic in `parse_file`. Move `clear_file_contributions` from RDoc::Server to RDoc::Store where it naturally belongs — it operates entirely on store internals (files_hash, classes_hash, modules_hash). Add tests for Store#clear_file_contributions covering single-file removal, multi-file preservation, per-file cleanup of methods/ constants/includes, and no-op for nonexistent files.
- Use exit instead of return in document method for consistency - Replace path.sub regex with delete_prefix in server.rb - Use to_json for safe JavaScript value embedding
Also update clear_file_contributions for the Hash-based comment_location: - Add keep_position: keyword for server re-parse (preserves key position) - Add rebuild_comment_from_location method - Server passes keep_position: true when re-parsing changed files
When keep_position: true is used during server re-parse, empty placeholder entries persist in comment_location if the re-parsed file no longer defines that class. Add Store#cleanup_stale_contributions to remove empty entries and stale in_files references after all re-parsing is complete. If a class has no remaining contributions, it is removed from the store.