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 (3)

Collapse
 
pawel_nowak profile image
Pawel •

Safe mode being the default is the feature that would make me actually install this. A tool that rewrites my hand-written types on its own is one I would stop trusting after the first bad merge. The source field is a smart touch too: suggesting the right fix per case is what turns this from another report nobody reads into something that actually gets used.

Collapse
 
unurgunite profile image
[NSObject init] •

Thanks, Pawel. Safe by default is a deliberate choice. A tool that rewrites a hand-written type once loses trust for good.

The source: field came from the editor plugins. I needed a way to offer "Update types" for RBS-backed fixes and plain "Fix YARD" for the rest.

Would be glad to see you as a user. Start small and tell me where it gets types wrong. I am writing a series on working with the gem and real type data (schema columns, returns, associations, two-way RBS sync). Next part lands in a few days.

Collapse
 
pawel_nowak profile image
Pawel •

I will give it a run on a small project and report back. The metaprogramming gap is exactly where I expect my codebase to stress it, so that will be the real test. Appreciate you taking feedback in the thread.