From 5042e81e097078d5f043b97f54d812bd22d61d92 Mon Sep 17 00:00:00 2001 From: "Stanislav (Stas) Katkov" Date: Mon, 17 Aug 2026 15:18:37 +0200 Subject: [PATCH 1/2] Deduplicate symlinked source files before parsing The fix deduplicates discovered files using File.realpath while retaining the first logical path. The same filtering is applied to the live-server watcher, preventing the alternate path from being rediscovered later. --- lib/rdoc/rdoc.rb | 9 ++++++++- lib/rdoc/server.rb | 2 +- test/rdoc/rdoc_rdoc_test.rb | 14 ++++++++++++++ test/rdoc/rdoc_server_test.rb | 13 +++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/rdoc/rdoc.rb b/lib/rdoc/rdoc.rb index 0af5c86497..064f888b7f 100644 --- a/lib/rdoc/rdoc.rb +++ b/lib/rdoc/rdoc.rb @@ -124,7 +124,7 @@ def gather_files(files) file_list = normalized_file_list files, true, @options.exclude - file_list = remove_unparseable(file_list) + file_list = remove_duplicate_files(remove_unparseable(file_list)) if file_list.count {|name, mtime| file_list[name] = @last_modified[name] unless mtime @@ -458,6 +458,13 @@ def remove_unparseable(files) end end + ## + # Removes duplicate canonical paths while preserving the first path found. + + def remove_duplicate_files(files) + files.uniq { |file,| File.realpath(file) }.to_h + end + ## # Generates documentation or a coverage report depending upon the settings # in +options+. diff --git a/lib/rdoc/server.rb b/lib/rdoc/server.rb index f3e13f6ab9..a5b4862f6b 100644 --- a/lib/rdoc/server.rb +++ b/lib/rdoc/server.rb @@ -399,7 +399,7 @@ def current_watch_files @options.files.empty? ? [@options.root.to_s] : @options.files, true, @options.exclude ) - @rdoc.remove_unparseable(file_list).keys | @rdoc.auto_discovered_rbs_signature_files + @rdoc.remove_duplicate_files(@rdoc.remove_unparseable(file_list)).keys | @rdoc.auto_discovered_rbs_signature_files end def file_changed?(file) diff --git a/test/rdoc/rdoc_rdoc_test.rb b/test/rdoc/rdoc_rdoc_test.rb index 7da44a9979..f804823d28 100644 --- a/test/rdoc/rdoc_rdoc_test.rb +++ b/test/rdoc/rdoc_rdoc_test.rb @@ -338,6 +338,20 @@ def test_gather_files assert_equal [a, b], @rdoc.gather_files([b, a, b]) end + def test_gather_files_deduplicates_symlinked_source_tree + temp_dir do |dir| + source_dir = File.join dir, 'gem' + symlink_dir = File.join dir, 'docs', 'gem' + FileUtils.mkdir_p [File.dirname(symlink_dir), source_dir] + FileUtils.touch File.join(source_dir, 'example.rb') + FileUtils.ln_s '../gem', symlink_dir + + assert_equal [File.join(symlink_dir, 'example.rb')], @rdoc.gather_files([dir]) + end + rescue NotImplementedError, Errno::EACCES, Errno::EPERM + omit 'symlinks are not supported' + end + def test_handle_pipe $stdin = StringIO.new "hello" diff --git a/test/rdoc/rdoc_server_test.rb b/test/rdoc/rdoc_server_test.rb index 09dd626a4f..e1489bf49e 100644 --- a/test/rdoc/rdoc_server_test.rb +++ b/test/rdoc/rdoc_server_test.rb @@ -126,4 +126,17 @@ def greet: () -> String greet = sample.find_method 'greet', false assert_equal ['() -> String'], greet.type_signature_lines end + + def test_current_watch_files_deduplicates_symlinked_source_tree + source_dir = File.join @dir, 'gem' + symlink_dir = File.join @dir, 'docs', 'gem' + FileUtils.mkdir_p [File.dirname(symlink_dir), source_dir] + FileUtils.touch File.join(source_dir, 'example.rb') + FileUtils.ln_s '../gem', symlink_dir + + files = @server.send :current_watch_files + assert_equal [File.join(symlink_dir, 'example.rb')], files.grep(/gem\/example\.rb\z/) + rescue NotImplementedError, Errno::EACCES, Errno::EPERM + omit 'symlinks are not supported' + end end From 0bf78a495bf74b83fccf1d2ebb2731f77694e41a Mon Sep 17 00:00:00 2001 From: "Stanislav (Stas) Katkov" Date: Mon, 17 Aug 2026 15:32:00 +0200 Subject: [PATCH 2/2] handle windows slashes --- test/rdoc/rdoc_rdoc_test.rb | 6 ++++-- test/rdoc/rdoc_server_test.rb | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/test/rdoc/rdoc_rdoc_test.rb b/test/rdoc/rdoc_rdoc_test.rb index f804823d28..67f4b01f38 100644 --- a/test/rdoc/rdoc_rdoc_test.rb +++ b/test/rdoc/rdoc_rdoc_test.rb @@ -343,10 +343,12 @@ def test_gather_files_deduplicates_symlinked_source_tree source_dir = File.join dir, 'gem' symlink_dir = File.join dir, 'docs', 'gem' FileUtils.mkdir_p [File.dirname(symlink_dir), source_dir] - FileUtils.touch File.join(source_dir, 'example.rb') + source_file = File.join source_dir, 'example.rb' + FileUtils.touch source_file FileUtils.ln_s '../gem', symlink_dir + omit 'directory symlinks are not supported' unless File.directory? symlink_dir - assert_equal [File.join(symlink_dir, 'example.rb')], @rdoc.gather_files([dir]) + assert_equal 1, @rdoc.gather_files([dir]).count { |file| File.identical?(source_file, file) } end rescue NotImplementedError, Errno::EACCES, Errno::EPERM omit 'symlinks are not supported' diff --git a/test/rdoc/rdoc_server_test.rb b/test/rdoc/rdoc_server_test.rb index e1489bf49e..1a8a0f9909 100644 --- a/test/rdoc/rdoc_server_test.rb +++ b/test/rdoc/rdoc_server_test.rb @@ -131,11 +131,12 @@ def test_current_watch_files_deduplicates_symlinked_source_tree source_dir = File.join @dir, 'gem' symlink_dir = File.join @dir, 'docs', 'gem' FileUtils.mkdir_p [File.dirname(symlink_dir), source_dir] - FileUtils.touch File.join(source_dir, 'example.rb') + source_file = File.join source_dir, 'example.rb' + FileUtils.touch source_file FileUtils.ln_s '../gem', symlink_dir + omit 'directory symlinks are not supported' unless File.directory? symlink_dir - files = @server.send :current_watch_files - assert_equal [File.join(symlink_dir, 'example.rb')], files.grep(/gem\/example\.rb\z/) + assert_equal 1, @server.send(:current_watch_files).count { |file| File.identical?(source_file, file) } rescue NotImplementedError, Errno::EACCES, Errno::EPERM omit 'symlinks are not supported' end