Skip to content

Commit f1985a8

Browse files
committed
Track RBS sig file freshness, propagate to aliases, separate singleton attr keys
Three fixes for `merge_rbs_signatures` and the surrounding freshness flow: - `rdoc.rb` now records `sig/*.rbs` mtimes alongside Ruby files in `@last_modified` and forces a full reparse when only signatures changed. The cached HTML pipeline keeps no in-memory class data across runs, so without this `merge_rbs_signatures` would run on an empty store. Also surfaces `rbs_signature_files`/`watch_files` for the live server's watcher. - `store.rb` now propagates merged RBS signatures to aliases immediately during assignment by walking `method_attr.aliases`, and tracks assigned objects so a re-merge in the live-server reload path can clear stale signatures without nuking inline `#:` annotations. - `rbs_helper.rb` keys singleton attributes as `Klass.attr` and instance attributes as `Klass#attr`, mirroring the method key format and preventing a singleton attr from clobbering an instance attr (or vice versa) when both share a name. Drive-by cleanups while in the area: fold `.rbs` into `remove_unparseable`'s extension regex; collapse the two identical `rbs_*_key` helpers into one `rbs_key`; drop a redundant `File.directory?` guard before `Dir[]`.
1 parent 60692c8 commit f1985a8

8 files changed

Lines changed: 319 additions & 19 deletions

File tree

lib/rdoc/rbs_helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def load_signatures(*dirs)
5858
sigs = member.overloads.map { |o| o.method_type.to_s }
5959
signatures[key] ||= sigs
6060
when RBS::AST::Members::AttrReader, RBS::AST::Members::AttrWriter, RBS::AST::Members::AttrAccessor
61-
key = "#{class_name}##{member.name}"
61+
key = member.kind == :singleton ? "#{class_name}.#{member.name}" : "#{class_name}##{member.name}"
6262
signatures[key] ||= [member.type.to_s]
6363
end
6464
end

lib/rdoc/rdoc.rb

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ def parse_files(files)
430430

431431
def remove_unparseable(files)
432432
files.reject do |file, *|
433-
file =~ /\.(?:class|eps|erb|scpt\.txt|svg|ttf|yml)$/i or
433+
file =~ /\.(?:class|eps|erb|rbs|scpt\.txt|svg|ttf|yml)$/i or
434434
(file =~ /tags$/i and
435435
/\A(\f\n[^,]+,\d+$|!_TAG_)/.match?(File.binread(file, 100)))
436436
end
@@ -470,6 +470,7 @@ def document(options)
470470
@store.load_cache
471471

472472
parse_files @options.files
473+
record_rbs_signature_mtimes
473474

474475
@options.default_title = "RDoc Documentation"
475476

@@ -488,7 +489,14 @@ def document(options)
488489

489490
@store.load_cache
490491

492+
rbs_signatures_changed = rbs_signatures_changed?
493+
# When only sig/*.rbs changed, no Ruby file would be reparsed and the
494+
# cached HTML pipeline keeps no in-memory class data across runs, so
495+
# force a full reparse to give merge_rbs_signatures a populated store.
496+
@last_modified.clear if rbs_signatures_changed
497+
491498
file_info = parse_files @options.files
499+
record_rbs_signature_mtimes
492500

493501
@options.default_title = "RDoc Documentation"
494502

@@ -502,7 +510,7 @@ def document(options)
502510
puts
503511

504512
puts @stats.report
505-
elsif file_info.empty? then
513+
elsif file_info.empty? && !rbs_signatures_changed then
506514
$stderr.puts "\nNo newer files." unless @options.quiet
507515
else
508516
gen_klass = @options.generator
@@ -553,11 +561,60 @@ def load_rbs_signatures
553561
sig_dir = File.join(@options.root.to_s, 'sig')
554562
sig_dirs << sig_dir if File.directory?(sig_dir)
555563
signatures = RDoc::RbsHelper.load_signatures(*sig_dirs)
556-
@store.merge_rbs_signatures(signatures) unless signatures.empty?
564+
@store.merge_rbs_signatures(signatures)
557565
rescue RBS::ParsingError, Errno::ENOENT, LoadError => e
558566
@options.warn "Failed to load RBS type signatures: #{e.message}"
559567
end
560568

569+
##
570+
# Returns all RBS signature files for the project.
571+
572+
def rbs_signature_files
573+
Dir[File.join(@options.root.to_s, 'sig', '**', '*.rbs')].sort
574+
end
575+
576+
##
577+
# Returns true if any RBS signature file has changed since the last run.
578+
579+
def rbs_signatures_changed?
580+
current = rbs_signature_mtimes
581+
previous = @last_modified.select { |file, _| File.extname(file) == '.rbs' }
582+
583+
return true unless (previous.keys - current.keys).empty?
584+
585+
current.any? do |file, mtime|
586+
last_modified = @last_modified[file]
587+
last_modified.nil? || mtime.to_i > last_modified.to_i
588+
end
589+
end
590+
591+
##
592+
# Records RBS signature file mtimes so normal generation freshness checks
593+
# and the live server watcher can see signature-only edits.
594+
595+
def record_rbs_signature_mtimes
596+
@last_modified.reject! { |file, _| File.extname(file) == '.rbs' }
597+
@last_modified.merge! rbs_signature_mtimes
598+
end
599+
600+
##
601+
# Files watched by the live preview server.
602+
603+
def watch_files
604+
(@last_modified.keys + rbs_signature_files).uniq
605+
end
606+
607+
def rbs_signature_file?(file) # :nodoc:
608+
File.extname(file) == '.rbs'
609+
end
610+
611+
def rbs_signature_mtimes # :nodoc:
612+
rbs_signature_files.each_with_object({}) do |file, mtimes|
613+
mtime = File.mtime(file) rescue nil
614+
mtimes[file] = mtime if mtime
615+
end
616+
end
617+
561618
##
562619
# Starts a live-reloading HTTP server for previewing documentation.
563620
# Called from #document when <tt>--server</tt> is given.

lib/rdoc/server.rb

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def start
8484
@tcp_server = TCPServer.new('127.0.0.1', @port)
8585
@running = true
8686

87-
@watcher_thread = start_watcher(@rdoc.last_modified.keys)
87+
@watcher_thread = start_watcher(@rdoc.watch_files)
8888

8989
url = "http://localhost:#{@port}"
9090
$stderr.puts "\nServing documentation at: \e]8;;#{url}\e\\#{url}\e]8;;\e\\"
@@ -294,9 +294,7 @@ def inject_live_reload(html, last_change_time)
294294
# re-parsing when changes are detected.
295295

296296
def start_watcher(source_files)
297-
@file_mtimes = source_files.each_with_object({}) do |f, h|
298-
h[f] = File.mtime(f) rescue nil
299-
end
297+
@file_mtimes = file_mtimes_for(source_files)
300298

301299
Thread.new do
302300
while @running
@@ -310,23 +308,41 @@ def start_watcher(source_files)
310308
end
311309
end
312310

311+
def file_mtimes_for(files)
312+
files.each_with_object({}) do |f, h|
313+
h[f] = File.mtime(f) rescue nil
314+
end
315+
end
316+
313317
##
314318
# Checks for modified, new, and deleted files. Returns true if any
315319
# changes were found and processed.
316320

317321
def check_for_changes
318322
changed = []
319323
removed = []
324+
changed_rbs = []
325+
removed_rbs = []
320326

321327
@file_mtimes.each do |file, old_mtime|
322328
unless File.exist?(file)
323-
removed << file
329+
if @rdoc.rbs_signature_file?(file)
330+
removed_rbs << file
331+
else
332+
removed << file
333+
end
324334
next
325335
end
326336

327337
current_mtime = File.mtime(file) rescue nil
328338
next unless current_mtime
329-
changed << file if old_mtime.nil? || current_mtime > old_mtime
339+
next unless old_mtime.nil? || current_mtime > old_mtime
340+
341+
if @rdoc.rbs_signature_file?(file)
342+
changed_rbs << file
343+
else
344+
changed << file
345+
end
330346
end
331347

332348
file_list = @rdoc.normalized_file_list(
@@ -341,17 +357,26 @@ def check_for_changes
341357
end
342358
end
343359

344-
return false if changed.empty? && removed.empty?
360+
@rdoc.rbs_signature_files.each do |file|
361+
unless @file_mtimes.key?(file)
362+
@file_mtimes[file] = nil
363+
changed_rbs << file
364+
end
365+
end
366+
367+
return false if changed.empty? && removed.empty? && changed_rbs.empty? && removed_rbs.empty?
368+
369+
removed_rbs.each { |file| @file_mtimes.delete(file) }
345370

346-
reparse_and_refresh(changed, removed)
371+
reparse_and_refresh(changed, removed, rbs_changed: !changed_rbs.empty? || !removed_rbs.empty?)
347372
true
348373
end
349374

350375
##
351376
# Re-parses changed files, removes deleted files from the store,
352377
# refreshes the generator, and invalidates caches.
353378

354-
def reparse_and_refresh(changed_files, removed_files)
379+
def reparse_and_refresh(changed_files, removed_files, rbs_changed: false)
355380
@mutex.synchronize do
356381
unless removed_files.empty?
357382
$stderr.puts "Removed: #{removed_files.join(', ')}"
@@ -385,6 +410,17 @@ def reparse_and_refresh(changed_files, removed_files)
385410

386411
@store.complete(@options.visibility)
387412

413+
if rbs_changed || !changed_files.empty?
414+
duration_ms = measure do
415+
@rdoc.load_rbs_signatures
416+
@rdoc.record_rbs_signature_mtimes
417+
@rdoc.rbs_signature_files.each do |file|
418+
@file_mtimes[file] = File.mtime(file) rescue nil
419+
end
420+
end
421+
$stderr.puts "Reloaded RBS signatures (#{duration_ms}ms)" if rbs_changed
422+
end
423+
388424
@generator.refresh_store_data
389425
@page_cache.clear
390426
@last_change_time = Time.now.to_f

lib/rdoc/store.rb

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -377,31 +377,59 @@ def type_name_lookup
377377
# Inline #: annotations take priority and are not overwritten.
378378

379379
def merge_rbs_signatures(signatures)
380+
clear_rbs_signatures
381+
380382
all_classes_and_modules.each do |cm|
381383
cm.method_list.each do |method|
382384
next if method.type_signature_lines
383385

384-
key = method.singleton ? "#{cm.full_name}.#{method.name}" : "#{cm.full_name}##{method.name}"
385-
sig = signatures[key]
386+
sig = signatures[rbs_key(cm, method)]
386387

387388
# RBS keys constructors as #initialize, but RDoc renames them to .new
388389
if !sig && method.name == 'new' && method.singleton
389390
sig = signatures["#{cm.full_name}#initialize"]
390391
end
391392

392-
method.type_signature_lines = sig if sig
393+
assign_rbs_signature method, sig if sig
393394
end
394395

395396
cm.attributes.each do |attr|
396397
next if attr.type_signature_lines
397398

398-
if (sig = signatures["#{cm.full_name}##{attr.name}"])
399-
attr.type_signature_lines = sig
399+
if (sig = signatures[rbs_key(cm, attr)])
400+
assign_rbs_signature attr, sig
400401
end
401402
end
402403
end
403404
end
404405

406+
def assign_rbs_signature(method_attr, signature) # :nodoc:
407+
@rbs_signature_method_attrs ||= []
408+
409+
method_attr.type_signature_lines = signature
410+
@rbs_signature_method_attrs << method_attr
411+
412+
method_attr.aliases.each do |aliased|
413+
next if aliased.type_signature_lines
414+
aliased.type_signature_lines = signature
415+
@rbs_signature_method_attrs << aliased
416+
end
417+
end
418+
419+
def clear_rbs_signatures # :nodoc:
420+
return unless @rbs_signature_method_attrs
421+
422+
@rbs_signature_method_attrs.each do |method_attr|
423+
method_attr.type_signature_lines = nil
424+
end
425+
426+
@rbs_signature_method_attrs.clear
427+
end
428+
429+
def rbs_key(cm, method_attr) # :nodoc:
430+
method_attr.singleton ? "#{cm.full_name}.#{method_attr.name}" : "#{cm.full_name}##{method_attr.name}"
431+
end
432+
405433
##
406434
# All TopLevels known to RDoc
407435

test/rdoc/rbs_helper_test.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ def greet: (String name) -> void
4040
end
4141
end
4242

43+
def test_load_signatures_keeps_instance_and_singleton_attributes_separate
44+
Dir.mktmpdir do |dir|
45+
File.write(File.join(dir, 'test.rbs'), <<~RBS)
46+
class Greeter
47+
attr_reader language: String
48+
attr_reader self.language: Integer
49+
end
50+
RBS
51+
52+
sigs = RDoc::RbsHelper.load_signatures(dir)
53+
assert_equal ['String'], sigs['Greeter#language']
54+
assert_equal ['Integer'], sigs['Greeter.language']
55+
end
56+
end
57+
4358
def test_signature_to_html_links_known_types
4459
lookup = { 'String' => 'String.html', 'Integer' => 'Integer.html' }
4560
result = RDoc::RbsHelper.signature_to_html(["(String) -> Integer"], lookup: lookup, from_path: 'Test.html')

test/rdoc/rdoc_rdoc_test.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22
require_relative 'helper'
33

44
class RDocRDocTest < RDoc::TestCase
5+
class RegenerationTrackingGenerator
6+
class << self
7+
attr_accessor :generated_store
8+
end
9+
10+
def initialize(store, _options)
11+
@store = store
12+
end
13+
14+
def generate
15+
self.class.generated_store = @store
16+
end
17+
end
518

619
def setup
720
super
@@ -40,6 +53,63 @@ def test_document # functional test
4053
assert_equal 'title', store.title
4154
end
4255

56+
def test_document_regenerates_when_rbs_file_changed
57+
temp_dir do |dir|
58+
source = File.join dir, 'example.rb'
59+
sig_dir = File.join dir, 'sig'
60+
sig = File.join sig_dir, 'example.rbs'
61+
output_dir = File.join dir, 'doc'
62+
63+
FileUtils.mkdir_p sig_dir
64+
FileUtils.mkdir_p output_dir
65+
66+
File.write source, <<~RUBY
67+
class Example
68+
def greet
69+
end
70+
end
71+
RUBY
72+
73+
File.write sig, <<~RBS
74+
class Example
75+
def greet: () -> String
76+
end
77+
RBS
78+
79+
source_mtime = Time.at 1000
80+
old_sig_mtime = Time.at 1000
81+
new_sig_mtime = Time.at 2000
82+
FileUtils.touch source, mtime: source_mtime
83+
FileUtils.touch sig, mtime: new_sig_mtime
84+
85+
File.open @rdoc.output_flag_file(output_dir), 'w' do |io|
86+
io.puts Time.at(1500).rfc2822
87+
io.puts "#{source}\t#{source_mtime.rfc2822}"
88+
io.puts "#{sig}\t#{old_sig_mtime.rfc2822}"
89+
end
90+
91+
options = RDoc::Options.new
92+
options.files = [source]
93+
options.root = Pathname dir
94+
options.op_dir = output_dir
95+
options.force_update = false
96+
options.quiet = true
97+
options.generator = RegenerationTrackingGenerator
98+
RegenerationTrackingGenerator.generated_store = nil
99+
100+
capture_output do
101+
RDoc::RDoc.new.document options
102+
end
103+
104+
store = RegenerationTrackingGenerator.generated_store
105+
refute_nil store
106+
107+
example = store.find_class_or_module 'Example'
108+
greet = example.find_method 'greet', false
109+
assert_equal ['() -> String'], greet.type_signature_lines
110+
end
111+
end
112+
43113
def test_document_with_dry_run # functional test
44114
options = RDoc::Options.new
45115
options.files = [File.expand_path('../xref_data.rb', __FILE__)]

0 commit comments

Comments
 (0)