Skip to content

Skip yardoc generation for gems the RBS collection already covers - #130

Draft
apiology wants to merge 8 commits into
masterfrom
skip-yard-when-rbs-suffices
Draft

apiology wants to merge 8 commits into
masterfrom
skip-yard-when-rbs-suffices

Conversation

@apiology

Copy link
Copy Markdown
Owner

This PR was written by Claude Code on behalf of @apiology.

Carried on the fork after castwide#1339 was closed as not planned, in favour of castwide#1378. The specs below are the part worth keeping alive regardless of which implementation wins.

Problem: solargraph cache parser shells out to yardoc across the gem's whole source tree even when the RBS collection already declares the types Solargraph reads from it.

$ solargraph cache parser
# yardoc runs over all of parser/lib, and the RBS collection
# then supplies the types anyway

Solution: ask the RBS collection whether it covers a gem before caching it, and skip Yardoc.load! when it does.

spec/suppress_yard_spec.rb pins the behaviour from outside the implementation: Yardoc.load! is not reached for a covered gem, the RBS-declared type on Parser::AST::Node#children still resolves, and a gem outside the collection still gets its yardoc. All three pass against castwide master at 2b9e317.

apiology and others added 8 commits September 5, 2026 23:33
Building YARD documentation for the parser gem takes over a minute on
a cold cache, while its RBS collection types already describe the
API. Caching parser measured 73.13s with YARD and 0.08s without.

PinCache.suppress_yard_cache? reports true for a gem named in
YARD_SUPPRESSED_GEMS whose RBS types resolved. Every place that
decides to build YARD pins now consults it: DocMap#cache,
Shell#cache, the named-gem path in Shell#gems, and Shell#do_cache.

DocMap#deserialize_combined_pin_cache treats a suppressed gem as
having an empty YARD half rather than an uncached one, so combined
pins still get written and the gem stops reading as uncached on
every load.

The trade-off is coverage: parser yields 9,175 combined pins with
YARD and 178 from RBS alone. Much of that gap is likely private and
generated node classes, but that has not been verified against what
users complete on.
undercover reported the method as 0.0% covered on this branch. The
existing #cache and #gems examples reach it only through a subprocess
shim, so none of its lines record coverage.

The new examples call the private method directly with a stubbed
Workspace, RbsMap and PinCache, and let the real
PinCache.suppress_yard_cache? decide: a suppressed gem whose RBS
collection resolves skips the YARD build, an unresolved cache key does
not, and a gem outside the suppression list does not.
The existing specs for this branch all reach through PinCache, GemPins
and RbsMap.from_gemspec, so they assert how the current caching layer is
built rather than what a user gets. castwide#1369 deletes all
three, which would leave the behaviour with no coverage at all.

Yardoc.load! is the one step on the YARD path that both the DocMap and
the Collection implementations reach unconditionally, so asserting on it
holds whichever one is caching the gem. Rebuild is forced so the
decision under test is the suppression rather than whatever the gem has
cached already; Thor reads the option by symbol, and a plain
string-keyed Hash reads back as nil, which silently made an earlier
draft of these pass on cache state instead.

Verified in both directions: three pass on this branch, and with
doc_map.rb, pin_cache.rb and shell.rb restored from 6dcb733 exactly
one fails, the parser-skips-YARD example. The other two are a control
and a guard against a suppression that also drops the RBS types, and
pass either way by design.
The eleven specs added by this branch all reached through PinCache,
GemPins, DocMap or RbsMap.from_gemspec. castwide#1369 deletes
every one of those, so the specs assert how today's caching layer is
built rather than what a user gets, and would leave the behaviour
uncovered the moment that lands.

Shell#cache and Shell#gems are public commands in both designs, and
Yardoc.load! is the one step on the YARD path that the DocMap and the
Collection implementations both reach unconditionally. Five examples on
those seams carry the same intent: the gem is not documented by YARD,
its RBS types still resolve, and a gem outside the list is unaffected,
each checked through both commands.

Removed in exchange: spec/pin_cache_spec.rb, the doc_map_spec context,
and the shell_spec do_cache block.

Two assertions have no public-seam equivalent and are simply gone. A
listed gem whose RBS fails to resolve cannot be set up without stubbing
RbsMap.from_gemspec, which is one of the removed APIs. Shell#do_cache is
private, has no counterpart in 1369, and its caller iterates every
installed gemspec, so the named-gem path in Shell#gems stands in for it.

Verified in both directions: 39 pass here, and with doc_map.rb,
pin_cache.rb and shell.rb restored from 6dcb733 exactly two fail, the
two that assert YARD is skipped.
Every line this branch changed lived in doc_map.rb, pin_cache.rb or the
part of shell.rb that drove them, and castwide#1369 deleted
the first two and rewrote the third. There is nothing here to reconcile
line by line, so the merge takes master whole and the behaviour comes
back in the next commit, written against Collection::Gem.

The five specs this branch added to shell_spec.rb go with it; they
return as spec/suppress_yard_spec.rb, which reaches the same
Yardoc.load! through the commands rather than through a DocMap.
Opening a workspace runs yardoc over every cacheable gem it requires,
and for `parser` that is over a minute of work whose pins an RBS
collection already carries. The five specs ported from
castwide#1339 measure it: before this change the two parser
examples ran 15.3 and 13.5 seconds against 5.9 and 5.6 for the backport
controls, and the whole file took 85 seconds. It now takes 5.

Collection::Gem skips Yardoc.load! for a gem on its list when the
workspace collection carries that gem, and returns the gem RBS pins
alone. The collection pins arrive as they already did, workspace-wide,
from External#load_rbs_collection.

A gem read without its YARD documentation holds different pins from one
read with it, and Metagem#cache_name is a bare basename shared by every
workspace on the machine, so the two variants get separate entries and
uncache clears both. Every caller that loads or asks after a gem
collection now says which variant it means: External#process_gem,
External#cache_changed?, ApiMap.load_with_cache, and the three Shell
commands.

The collection reading moves out of External into RbsCollection, because
Shell needs the same answer and building an External to get it would
parse every collection signature first. External keeps its two accessors
and delegates.

Base.uncache now goes through an instance method so Gem can clear both
of its entries without duplicating the removal.

Verified against master: the five specs fail there, two on
`Yardoc.load!` having been received. Full suite 1619 examples, 0
failures, 78 pending.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E8ckZEYYMDXe9eDNnyxaim
The only conflict was shell.rb. castwide#1373 folded the cache
command into gems and renamed the result to cache, leaving gems a Thor
alias, so both sides had rewritten the same two methods.

Master's structure is taken whole, with this branch contribution put back
on top of it: Collection::Gem.load receives rbs_collection:, so a gem whose
RBS collection data suffices still skips the expensive YARD generation this
branch exists to avoid. The standalone cache GEM command is dropped,
because master consolidated command covers it.

Both sides had written a @PARAM tag for the renamed parameter; the
surviving one names gem_names.
castwide#1373 left gems a Thor CLI alias rather than a method,
so the two examples calling it on an instance raised NoMethodError once
this branch followed master structure.

Both existed to cover gems as a second way in, and both assert exactly
what the cache example beside them already does -- one that YARD is not
read for a gem on the list, the other that it is read for a gem outside
it. With the second entry point gone there is nothing left for them to
cover, so retargeting them at cache would only duplicate coverage.
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.

1 participant