DEV Community

Cover image for Keeping YARD and RBS in sync without going insane
[NSObject init]
[NSObject init]

Posted on

Keeping YARD and RBS in sync without going insane

I run docscribe daily on client Rails apps. I cannot share that code, so every example below is synthetic and verified against docscribe 1.6.2 on Ruby 3.4.5.

Comments lie

This is the shape I keep finding:

# @param [String] count
# @return [String]
def notify(verbose:, count:)
  puts "verbose!" if verbose  # Boolean
  count.times { send_ping }   # Integer
  count                       # Integer
end
Enter fullscreen mode Exit fullscreen mode

The comment says two Strings in, one String out. The code takes a flag and a count and returns the count. Anyone who trusts the comment calls it wrong: "false" is truthy, and "3" has no times method.

before/after YARD fix

What docscribe -a actually does

Safe mode adds what is missing and leaves the rest alone. On the file above:

$ docscribe -a notify_demo.rb
Enter fullscreen mode Exit fullscreen mode
# @param [String] count
# @param [Object] verbose Param documentation.
# @return [String]
def notify(verbose:, count:)
  puts "verbose!" if verbose
  count.times { send_ping }
  count
end
Enter fullscreen mode Exit fullscreen mode

It added the missing @param verbose. It did not touch the lying String tags. That is the contract: safe mode never rewrites a type you wrote. -A rebuilds the block from scratch; I always git diff after it.

terminal run: cat, docscribe -a, cat

Where types really get fixed

Type correction lives in update_types, fed by RBS. Given this signature:

class Object
  def notify: (bool verbose, Integer count) -> Integer
end
Enter fullscreen mode Exit fullscreen mode

this command:

$ docscribe update_types --rbs --sig-dir sig notify.rb
Enter fullscreen mode Exit fullscreen mode

produces:

# @param [Boolean] verbose
# @param [Integer] count
# @return [Integer]
def notify(verbose:, count:)
  puts "verbose!" if verbose
  count.times { send_ping }
  count
end
Enter fullscreen mode Exit fullscreen mode

No prose placeholders here because the original had no descriptions to keep. When descriptions exist, update_types keeps them.

The reverse direction exists too: docscribe rbs generates .rbs from YARD. Types flow both ways, YARD stays the human-readable cache in the middle.

source: tells the editor which fix to offer

Every change in 1.6.2 carries source: rbs | infer | syntax through JSON, SARIF, and daemon responses. The IDE uses it to pick the right lightbulb: rbs offers "Update types", anything else offers "Fix YARD". That one field is what makes the VS Code and RubyMine plugins feel fast instead of noisy.

Rails specifics I use daily

Types from schema.rb, not from code. t.boolean :is_admin becomes @!attribute [r] is_admin returning Boolean in the model, via the collector plugin in examples/plugins. No hand-written docs for columns.

One daemon per project serves both editor plugins over a Unix socket JSON-RPC (check, fix, check_batch, update_types, ping). When the daemon is off, the plugins fall back to CLI. CI mode runs on exit codes (0 clean, 1 missing docs, 2 errors) plus check_for_comments, with json/sarif output.

Next in 1.7.0

docscribe sorbet - same YARD to signature flow as docscribe rbs, but generating .rbi/sig blocks for srb tc. Plus validate_types on by default (opt-out with --no-validate-types). Plus tighter metaprogramming support: better types for define_method and DSL-generated methods, less Object fallback.

Limits

Heuristics, not a prover, so metaprogramming lands in Object today. Each release narrows that gap.

Question I keep asking: do you keep YARD and RBS in sync by hand, or did you give up on one of them?

Top comments (0)