From 22e18fd0787221e4442bcf442833ebb4490caf9c Mon Sep 17 00:00:00 2001 From: Kazuhiro NISHIYAMA Date: Wed, 22 Jul 2026 12:51:22 +0900 Subject: [PATCH 1/2] =?UTF-8?q?4.0=20=E4=BB=A5=E9=99=8D=E3=81=AE=E3=83=89?= =?UTF-8?q?=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88=E3=81=A7=E3=83=A2?= =?UTF-8?q?=E3=82=B8=E3=83=A5=E3=83=BC=E3=83=AB=E9=96=A2=E6=95=B0=E3=81=AE?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E3=82=92=20=3F.=20=E3=81=AB=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 方針(#250 のコメントより): Ruby 4.0 以降のドキュメントでの表示変更を 目標にし、それまでは "." を維持する。ソース側は既に "?." 記法に対応 済み(markdown_to_rrd.rb が読み込み時に ".#" へ変換する)なので、今回は 表示側のみを対象にする。RBS 形式のメソッド定義行対応は別機能なので 含めない。 choke point: NameUtils.display_typemark(mark, version) を新設。typemark が '.#' かつ Gem::Version.new(version) >= Gem::Version.new('4.0') の ときだけ '?.' を返し、それ以外は無変換(文字列比較ではなく Gem::Version で比較する。"10.0" のような二桁メジャーを文字列比較すると "4.0" より 小さく見えてしまうため)。 label/short_label/labels/title_labels(methodentry.rb)は表示用途と 識別子用途の両方で使われている(labels は refsdatabase.rb の [[a:...]] アンカーキー、label/labels は `bitclust methods --diff` の 突き合わせが literal ".#" を前提にしている)ため、既存メソッドは一切 変更せず、display_typemark/display_label/display_short_label/ display_title_labels を新設してテンプレート側の表示箇所だけを付け替えた。 対象にした表示箇所: - MethodEntry#display_label/display_short_label/display_title_labels (methodentry.rb) — メソッドページの見出し・、htmlutils.rb の link_to_method(class-index・library ページの一覧) - RDCompiler#method_signature の @opt 分岐(rdcompiler.rb) — template.lillia/library がクラス名+typemark 接頭辞を付けて描画する箇所 - data/bitclust/template/class の「継承しているメソッド」一覧 — MethodSpec(@db を持たない値オブジェクト)を直接文字列化するので screen 側の @target_version 経由で display_typemark を呼ぶ - data/bitclust/template/search — bitclust server の /search が使う 動的 CGI 検索結果ページ - SearchIndexGenerator#method_entries(search_index_generator.rb) — 静的検索インデックスの表示ラベル(full_name)。path は typechar の ままなので不変 - ChmCommand(chm_command.rb)の .hhk 索引ラベル 識別子は一切変更していない: method id・URL(/method/Kernel/m/open 等)・ spec 文字列(lookup/parsing に使う ".#")・DB ファイル名・index id は すべて typemark()/typechar() 経由のまま。 調査のみ(未実装、フォローアップ候補): bitclust server の /search (SimpleSearcher#parse_method_spec_pattern)は "?." 区切りを認識せず、 "Kernel?.mf" のようなクエリはクラス名側に "?" が混入して検索がヒット しない。静的検索(theme/default/js/search_ranker.js)も "." を "::" に 正規化する処理が ".#"/"?." を考慮しておらず、修飾クエリ(区切り文字 つき)は module function では従来から不確実(無修飾のメソッド名検索は name フィールドが typemark と無関係なので影響なし)。 t_wada 式 TDD で実装(テスト→red→green): NameUtils#display_typemark の 単体テスト、MethodEntry の表示系4メソッド、RDCompiler の @opt 分岐、 SearchIndexGenerator、ClassScreen(継承一覧)、MethodScreen(見出し・ <title>)、SearchScreen(新規)を追加。既存スイート(バージョン指定なし のダミー DB 等)は無変更のまま green。17762 → 17787 tests(+25)、 bundle exec rake test / rbs validate / steep check すべて green。 refs #250 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- data/bitclust/template.epub/function | 4 +- data/bitclust/template.epub/method | 4 +- data/bitclust/template.lillia/method | 4 +- data/bitclust/template.offline/function | 4 +- data/bitclust/template.offline/method | 4 +- data/bitclust/template/class | 2 +- data/bitclust/template/function | 4 +- data/bitclust/template/method | 4 +- data/bitclust/template/search | 2 +- lib/bitclust/functionentry.rb | 4 ++ lib/bitclust/htmlutils.rb | 2 +- lib/bitclust/methodentry.rb | 32 ++++++++++ lib/bitclust/nameutils.rb | 15 +++++ lib/bitclust/rdcompiler.rb | 2 +- lib/bitclust/search_index_generator.rb | 5 +- lib/bitclust/subcommands/chm_command.rb | 4 +- sig/bitclust/functionentry.rbs | 1 + sig/bitclust/methodentry.rbs | 8 +++ sig/bitclust/nameutils.rbs | 4 ++ sig/bitclust/rdcompiler.rbs | 2 +- test/test_class_screen.rb | 84 +++++++++++++++++++++++++ test/test_method_screen.rb | 43 +++++++++++++ test/test_methodentry.rb | 79 +++++++++++++++++++++++ test/test_nameutils.rb | 22 +++++++ test/test_rdcompiler.rb | 41 ++++++++++++ test/test_search_index_generator.rb | 35 +++++++++++ test/test_search_screen.rb | 48 ++++++++++++++ 27 files changed, 442 insertions(+), 21 deletions(-) create mode 100644 test/test_search_screen.rb diff --git a/data/bitclust/template.epub/function b/data/bitclust/template.epub/function index aeebe8e4..9602442c 100644 --- a/data/bitclust/template.epub/function +++ b/data/bitclust/template.epub/function @@ -1,6 +1,6 @@ <% entry = @entry - @title = "#{entry.type_label} #{entry.label}" + @title = "#{entry.type_label} #{entry.display_label}" %> <p> <%= manual_home_link() %> @@ -10,7 +10,7 @@ </p> <% headline_init %> -<%= headline("#{entry.type_label} #{entry.label}") %> +<%= headline("#{entry.type_label} #{entry.display_label}") %> <dl class="functionlist"> <dt> <code><%= entry.header %></code> diff --git a/data/bitclust/template.epub/method b/data/bitclust/template.epub/method index 63c3d524..80928bb7 100644 --- a/data/bitclust/template.epub/method +++ b/data/bitclust/template.epub/method @@ -1,6 +1,6 @@ <% entry = @entries.sort.first - @title = "#{entry.type_label} #{entry.title_labels.join(', ')}" + @title = "#{entry.type_label} #{entry.display_title_labels.join(', ')}" %> <p> <%= manual_home_link() %> @@ -12,7 +12,7 @@ </p> <% headline_init %> -<%= headline("#{entry.type_label} #{entry.label}") %> +<%= headline("#{entry.type_label} #{entry.display_label}") %> <dl class="methodlist"> <% headline_push diff --git a/data/bitclust/template.lillia/method b/data/bitclust/template.lillia/method index 24b11304..9a5b160e 100644 --- a/data/bitclust/template.lillia/method +++ b/data/bitclust/template.lillia/method @@ -1,6 +1,6 @@ <% entry = @entries.sort.first - @title = "#{entry.type_label} #{entry.title_labels.join(', ')}" + @title = "#{entry.type_label} #{entry.display_title_labels.join(', ')}" @description = entry.description %> <p> @@ -13,7 +13,7 @@ </p> <% headline_init %> -<%= headline("#{entry.type_label} #{entry.label}") %> +<%= headline("#{entry.type_label} #{entry.display_label}") %> <dl class="methodlist"> <% headline_push diff --git a/data/bitclust/template.offline/function b/data/bitclust/template.offline/function index 3a946bf2..ea96e335 100644 --- a/data/bitclust/template.offline/function +++ b/data/bitclust/template.offline/function @@ -1,6 +1,6 @@ <% entry = @entry - @title = "#{entry.type_label} #{entry.label}" + @title = "#{entry.type_label} #{entry.display_label}" @description = @entry.description @edit_url = edit_url(@entry.source_location) if @conf[:edit_base_url] && @entry.source_location %> @@ -41,7 +41,7 @@ </ol> </nav> <% headline_init %> -<%= headline("#{entry.type_label} #{entry.label}", edit_url: @edit_url) %> +<%= headline("#{entry.type_label} #{entry.display_label}", edit_url: @edit_url) %> </header> <main> diff --git a/data/bitclust/template.offline/method b/data/bitclust/template.offline/method index b9af10f7..97a246ba 100644 --- a/data/bitclust/template.offline/method +++ b/data/bitclust/template.offline/method @@ -1,6 +1,6 @@ <% entry = @entries.sort.first - @title = entry.title_labels.join(', ') + @title = entry.display_title_labels.join(', ') @description = entry.description @edit_url = edit_url(entry.source_location) if @conf[:edit_base_url] && entry.source_location %> @@ -55,7 +55,7 @@ </ol> </nav> <% headline_init %> -<%= headline("#{entry.type_label} #{entry.label}") %> +<%= headline("#{entry.type_label} #{entry.display_label}") %> </header> <main> diff --git a/data/bitclust/template/class b/data/bitclust/template/class index babe9569..050dd411 100644 --- a/data/bitclust/template/class +++ b/data/bitclust/template/class @@ -107,7 +107,7 @@ <%= headline(_('Inherited Methods')) %> <p><code> <% specs.each do |s| %> -<%= method_link(s.to_s, (s.type == '#' ? s.method : s.type + s.method)) %> +<%= method_link(s.to_s, (s.type == '#' ? s.method : display_typemark(s.type, @target_version) + s.method)) %> <% end %> </code></p> <% end %> diff --git a/data/bitclust/template/function b/data/bitclust/template/function index 7ba9ebc3..5e6c29fd 100644 --- a/data/bitclust/template/function +++ b/data/bitclust/template/function @@ -1,5 +1,5 @@ <% - @title = "#{@entry.type_label} #{@entry.label}" + @title = "#{@entry.type_label} #{@entry.display_label}" @description = @entry.description %> <p> @@ -11,7 +11,7 @@ <%= search_form() %> <% headline_init %> -<%= headline("#{@entry.type_label} #{@entry.label}") %> +<%= headline("#{@entry.type_label} #{@entry.display_label}") %> <dl class="functionlist"> <dt> <code><%=h @entry.header %></code> diff --git a/data/bitclust/template/method b/data/bitclust/template/method index 4d563fd2..967ba52c 100644 --- a/data/bitclust/template/method +++ b/data/bitclust/template/method @@ -1,6 +1,6 @@ <% entry = @entries.sort.first - @title = "#{entry.type_label} #{entry.title_labels.join(', ')}" + @title = "#{entry.type_label} #{entry.display_title_labels.join(', ')}" @description = entry.description %> <p> @@ -15,7 +15,7 @@ <% headline_init %> -<%= headline("#{entry.type_label} #{entry.label}") %> +<%= headline("#{entry.type_label} #{entry.display_label}") %> <% headline_push %> <dl class="methodlist"> <% @entries.sort.each do |ent| %> diff --git a/data/bitclust/template/search b/data/bitclust/template/search index f6452db7..8c5f6b38 100644 --- a/data/bitclust/template/search +++ b/data/bitclust/template/search @@ -39,7 +39,7 @@ else sigs.map {|sig| method_link(e.spec_string, - e.klass.name + e.typemark + sig.to_s) + e.klass.name + e.display_typemark + sig.to_s) } end.join("<br>\n") %></td> <td class="description"><%= compile_rd(src) %></td> diff --git a/lib/bitclust/functionentry.rb b/lib/bitclust/functionentry.rb index 5f892946..692af488 100644 --- a/lib/bitclust/functionentry.rb +++ b/lib/bitclust/functionentry.rb @@ -52,6 +52,10 @@ def <=>(other) remove_method :name alias name id alias label id + # bitclust#250: MethodEntry の display_label と同じインターフェースを + # 満たすためのエイリアス。関数には module function の概念(typemark)が + # ないので単に label と同じ(変換なし) + alias display_label label alias macro? macro alias private? private diff --git a/lib/bitclust/htmlutils.rb b/lib/bitclust/htmlutils.rb index 0f3d32c3..87389dde 100644 --- a/lib/bitclust/htmlutils.rb +++ b/lib/bitclust/htmlutils.rb @@ -21,7 +21,7 @@ module HTMLUtils # make method anchor from MethodEntry def link_to_method(m, specp = false) - label = specp ? m.label : m.short_label + label = specp ? m.display_label : m.display_short_label a_href(@urlmapper.method_url(methodid2specstring(m.id)), label) end diff --git a/lib/bitclust/methodentry.rb b/lib/bitclust/methodentry.rb index bd96eea6..183ea7f1 100644 --- a/lib/bitclust/methodentry.rb +++ b/lib/bitclust/methodentry.rb @@ -74,6 +74,14 @@ def typechar methodid2typechar(@id) end + # bitclust#250: 表示専用の typemark。DB のバージョンが 4.0 以降なら + # module function の "." を "?." にする(それ以外の typemark は不変)。 + # typemark 自体(識別子として使う方)は変えない -- URL・spec 文字列・ + # refsdatabase のアンカーキー等はすべて typemark() 経由のまま + def display_typemark + NameUtils.display_typemark(typemark(), @db.propget('version')) + end + def type_label case typemark() when '.' then 'singleton method' @@ -131,11 +139,29 @@ def label "#{t == '$' ? '' : c}#{t}#{m}" end + # bitclust#250: label の表示専用版(module function は 4.0 以降 "?." で + # 表示)。label 自身は refsdatabase.rb の [[a:...]] アンカーキーや + # `bitclust methods --diff` の突き合わせが literal ".#" 前提で使うので + # 変えない -- テンプレート側の実際の表示箇所だけがこちらを呼ぶ + def display_label + c, t, m, _lib = methodid2specparts(@id) + "#{t == '$' ? '' : c}#{display_typemark}#{m}" + end + def short_label _c, t, m, _lib = methodid2specparts(@id) "#{t == '#' ? '' : t}#{m}" end + # bitclust#250: short_label の表示専用版。short_label 自身の唯一の + # 呼び出し元(htmlutils.rb の link_to_method)は display_short_label に + # 切り替え済みだが、short_label 自体は同じ理由(識別子的な使い方をする + # 将来のコードのため)で変えない + def display_short_label + _c, t, m, _lib = methodid2specparts(@id) + "#{t == '#' ? '' : display_typemark}#{m}" + end + def index_id "#{methodid2typechar(@id)}_#{encodename_fs(name).gsub(/=/, '--')}".upcase end @@ -154,6 +180,12 @@ def title_labels names().map {|name| "#{t == '$' ? '' : c}#{t}#{name}" } end + # bitclust#250: title_labels の表示専用版(<title> タグ・見出しで使う) + def display_title_labels + c, t, _m, _lib = methodid2specparts(@id) + names().map {|name| "#{t == '$' ? '' : c}#{display_typemark}#{name}" } + end + def name?(name) names().include?(name) end diff --git a/lib/bitclust/nameutils.rb b/lib/bitclust/nameutils.rb index c1266d3a..840002c6 100644 --- a/lib/bitclust/nameutils.rb +++ b/lib/bitclust/nameutils.rb @@ -207,6 +207,21 @@ def typemark2char(mark) raise "must not happen: #{mark.inspect}" end + # bitclust#250: Ruby 4.0 以降のドキュメントでは module function の表記を + # るりま独自の "." から "?." に変える(表示のみ。ソース側は既に "?." + # 記法に対応済みで、内部的には従来どおり ".#" に変換される)。 + # + # これは見た目だけの変換であり、method id・URL・spec 文字列など識別子と + # して使う ".#" 自体は一切変えない。typemark が '.#' 以外、または version + # が不明(nil/空)な場合はそのまま返す。バージョン比較は文字列比較では + # なく Gem::Version で行う("10.0" を文字列比較すると "4.0" より小さく + # 見えてしまうため)。 + def display_typemark(mark, version) + return mark unless mark == '.#' + return mark unless version && !version.empty? + Gem::Version.new(version) >= Gem::Version.new('4.0') ? '?.' : mark + end + def functionname?(n) /\A\w+\z/ =~ n ? true : false end diff --git a/lib/bitclust/rdcompiler.rb b/lib/bitclust/rdcompiler.rb index 3d2c6c39..23a070fc 100644 --- a/lib/bitclust/rdcompiler.rb +++ b/lib/bitclust/rdcompiler.rb @@ -434,7 +434,7 @@ def method_signature(sig_line, first) string %Q(<dt class="method-heading") string %Q( id="#{@method.index_id}") if first string '><code>' - string @method.klass.name + @method.typemark if @opt + string @method.klass.name + @method.display_typemark if @opt string escape_html(sig.friendly_string) string '</code>' string heading_version_badges(@method, sig.name, first) if @method diff --git a/lib/bitclust/search_index_generator.rb b/lib/bitclust/search_index_generator.rb index 2c65104c..e38ff80d 100644 --- a/lib/bitclust/search_index_generator.rb +++ b/lib/bitclust/search_index_generator.rb @@ -122,7 +122,10 @@ def method_entries(db) next unless type cname = entry.klass.name - tmark = entry.typemark + # bitclust#250: 検索結果に出る表示ラベル(name/full_name)は 4.0 + # 以降のドキュメントでは module function を "?." で表示する。path + # は識別子なので typechar(常に不変)のまま + tmark = entry.display_typemark tchar = entry.typechar entry.names.each do |mname| path = "method/#{encode(cname)}/#{tchar}/#{encode(mname)}#{@suffix}" diff --git a/lib/bitclust/subcommands/chm_command.rb b/lib/bitclust/subcommands/chm_command.rb index f363cdb8..f7292052 100644 --- a/lib/bitclust/subcommands/chm_command.rb +++ b/lib/bitclust/subcommands/chm_command.rb @@ -207,8 +207,10 @@ def exec(argv, options) name = e.typename == :special_variable ? "$#{e_name}" : e_name @index_contents << Sitemap::Content.new("#{name} (#{e.library.name} - #{e.klass.name})", filename) + # bitclust#250: CHM の索引(.hhk)に出る表示ラベル。4.0 以降の + # ドキュメントでは module function を "?." で表示する @index_contents << - Sitemap::Content.new("#{e.klass.name}#{e.typemark}#{name} (#{e.library.name})", filename) + Sitemap::Content.new("#{e.klass.name}#{e.display_typemark}#{name} (#{e.library.name})", filename) end end pb.title = align_progress_bar_title(e.name) diff --git a/sig/bitclust/functionentry.rbs b/sig/bitclust/functionentry.rbs index 49efead1..0acce0e1 100644 --- a/sig/bitclust/functionentry.rbs +++ b/sig/bitclust/functionentry.rbs @@ -20,6 +20,7 @@ module BitClust attr_accessor source_location: Location? alias label id + alias display_label label alias macro? macro alias private? private diff --git a/sig/bitclust/methodentry.rbs b/sig/bitclust/methodentry.rbs index 306ecad0..d61f78e0 100644 --- a/sig/bitclust/methodentry.rbs +++ b/sig/bitclust/methodentry.rbs @@ -28,6 +28,8 @@ module BitClust def typechar: () -> NameUtils::typechar + def display_typemark: () -> String + def type_label: () -> String def library: () -> LibraryEntry @@ -54,14 +56,20 @@ module BitClust def label: () -> String + def display_label: () -> String + def short_label: () -> String + def display_short_label: () -> String + def index_id: () -> String def labels: () -> Array[String] def title_labels: () -> Array[String] + def display_title_labels: () -> Array[String] + def name?: (String name) -> bool def since_of: (String name) -> String? diff --git a/sig/bitclust/nameutils.rbs b/sig/bitclust/nameutils.rbs index 1b42e505..38f8d798 100644 --- a/sig/bitclust/nameutils.rbs +++ b/sig/bitclust/nameutils.rbs @@ -97,6 +97,10 @@ module BitClust def self?.typemark2char: (typemark mark) -> typechar + # bitclust#250: 表示専用の変換。戻り値は '?.' も取り得るので typemark + # 型ではなく String にする(識別子として使ってはいけない)。 + def self?.display_typemark: (typemark mark, String? version) -> String + def self?.functionname?: (String n) -> bool # string -> case-sensitive ID diff --git a/sig/bitclust/rdcompiler.rbs b/sig/bitclust/rdcompiler.rbs index 4bf58bac..fde70e70 100644 --- a/sig/bitclust/rdcompiler.rbs +++ b/sig/bitclust/rdcompiler.rbs @@ -32,7 +32,7 @@ module BitClust type opt = bool? - # method_signature で `string @method.klass.name + @method.typemark` を実行するかどうか + # method_signature で `string @method.klass.name + @method.display_typemark` を実行するかどうか @opt: opt @f: LineInput diff --git a/test/test_class_screen.rb b/test/test_class_screen.rb index 48a13acc..5015801e 100644 --- a/test/test_class_screen.rb +++ b/test/test_class_screen.rb @@ -327,3 +327,87 @@ def test_per_signature_badges_attach_to_their_own_alias_line 'the until badge must attach after alias_two') end end + +# bitclust#250: Ruby 4.0 以降のドキュメントでは module function の表記を +# 独自の「.#」から「?.」に変える(表示のみ、識別子は変えない)。 +# +# data/bitclust/template/class の「継承しているメソッド」一覧は +# @entry.inherited_method_specs(MethodSpec、@db を持たない値オブジェクト)を +# 直接文字列化する数少ない箇所なので、上の TestClassScreenDefaultTemplateVersionBadges +# と同じ理由で実ディスク DB を使う +class TestClassScreenModuleFunctionDisplay < Test::Unit::TestCase + SRC = <<'HERE' += module Mixin +mixin module +== Module Functions +--- mf +mixin module function += class Host < Object +include Mixin +host class +== Instance Methods +--- own_method +own method +HERE + + def render(version) + datadir = File.expand_path('../data/bitclust', __dir__) + dbdir = Dir.mktmpdir('bitclust-module-function-display-db') + srcdir = Dir.mktmpdir('bitclust-module-function-display-src') + begin + src_path = File.join(srcdir, 'testlib.rd') + File.write(src_path, SRC) + + db = BitClust::MethodDatabase.new(dbdir) + db.init + db.transaction do + db.propset('version', version) + db.propset('encoding', 'utf-8') + end + db.transaction do + db.update_by_file(src_path, 'testlib') + end + + manager = BitClust::ScreenManager.new( + :templatedir => "#{datadir}/template", + :catalogdir => "#{datadir}/catalog", + :encoding => 'utf-8', + :default_encoding => 'utf-8', + :base_url => '', + :target_version => version + ) + # See TestClassScreenDefaultTemplateVersionBadges above for the general + # background: Screen#run_template caches the compiled ERB body on the + # *class*, keyed only by method name. A plain anonymous subclass is not + # sufficient isolation here, though: if BitClust::ClassScreen itself + # was already compiled for a different templatedir (offline) by some + # other test earlier in this same process -- order-dependent, since + # TestClassScreen above uses BitClust::ClassScreen directly, with no + # subclass -- then `respond_to?(:class_template)` is already true via + # *inheritance*, and the subclass would silently reuse that stale + # compiled method instead of compiling its own. So compile directly + # onto screen_class ourselves, before Screen#run_template ever gets a + # chance to check: our own method then always wins by normal Ruby + # method resolution, regardless of what BitClust::ClassScreen has. + screen_class = Class.new(BitClust::ClassScreen) + repo = BitClust::TemplateRepository.new("#{datadir}/template") + ERB.new(repo.load('class')).def_method(screen_class, 'class_template', 'class.erb') + manager.send(:new_screen, screen_class, db.get_class('Host'), :database => db).body + ensure + FileUtils.rm_r(srcdir, :force => true) + FileUtils.rm_r(dbdir, :force => true) + end + end + + def test_inherited_module_function_shows_dot_hash_before_4_0 + html = render('3.4') + assert_include(html, '.#mf') + assert_not_include(html, '?.mf') + end + + def test_inherited_module_function_switches_to_question_dot_at_4_0 + html = render('4.0') + assert_include(html, '?.mf') + assert_not_include(html, '.#mf') + end +end diff --git a/test/test_method_screen.rb b/test/test_method_screen.rb index de961e54..697b447c 100644 --- a/test/test_method_screen.rb +++ b/test/test_method_screen.rb @@ -121,3 +121,46 @@ def test_since_badge_is_rendered_after_fill_since assert_include(html, '<span class="method-since-badge">Ruby 3.2 から</span>') end end + +# bitclust#250: Ruby 4.0 以降のドキュメントでは module function の表記を +# 独自の「.#」から「?.」に変える(表示のみ、識別子は変えない)。この +# メソッドページの見出し・<title> がまさに issue に挙がっている +# 「module function Kernel.#open」の例そのもの +class TestMethodScreenModuleFunctionDisplay < Test::Unit::TestCase + SRC = <<'HERE' += module Kernel +description +== Module Functions +--- mf + +説明 +HERE + + def render(version) + _lib, db = BitClust::RRDParser.parse(SRC, 'testlib', {'version' => version}) + entry = db.get_method(BitClust::MethodSpec.parse('Kernel.#mf')) + datadir = File.expand_path('../data/bitclust', __dir__) + manager = BitClust::ScreenManager.new( + :templatedir => "#{datadir}/template.offline", + :catalogdir => "#{datadir}/catalog", + :encoding => 'utf-8', + :default_encoding => 'utf-8', + :base_url => '', + :target_version => version + ) + manager.method_screen([entry], :database => db).body + end + + def test_headline_and_title_show_dot_hash_before_4_0 + html = render('3.4') + assert_match(%r{<h1>module function Kernel\.#mf</h1>}, html) + assert_include(html, '<title>Kernel.#mf') + end + + def test_headline_and_title_switch_to_question_dot_at_4_0 + html = render('4.0') + assert_match(%r{<h1>module function Kernel\?\.mf</h1>}, html) + assert_include(html, '<title>Kernel?.mf') + assert_not_include(html, 'Kernel.#mf') + end +end diff --git a/test/test_methodentry.rb b/test/test_methodentry.rb index cfaaa972..2072d7a7 100644 --- a/test/test_methodentry.rb +++ b/test/test_methodentry.rb @@ -69,6 +69,85 @@ def test_title_labels_matches_label_for_single_special_variable end end +# bitclust#250: Ruby 4.0 以降のドキュメントでは module function の表記を +# 独自の「.#」から「?.」に変える(表示のみ。識別子は変えない)。 +# +# label/short_label/labels/title_labels 自身は refsdatabase.rb の +# [[a:...]] アンカー解決キー(labels)や `bitclust methods --diff` の +# expand_mf マッチング(labels)が literal ".#" を前提にしているため、 +# 一切変更しない。表示用には display_label/display_short_label/ +# display_title_labels/display_typemark を新設し、テンプレート側の +# 実際の表示箇所だけがそちらを呼ぶようにする。 +class TestMethodEntryDisplayTypemark < Test::Unit::TestCase + SRC = <<HERE += module Kernel +description +== Module Functions +--- mf +--- mf2 + +説明 + +== Instance Methods +--- im + +説明 + +== Special Variables +--- $stdout -> IO + +標準出力。 +HERE + + def build(version) + _lib, db = BitClust::RRDParser.parse(SRC, 'testlib', {'version' => version}) + db + end + + def test_module_function_display_stays_dot_hash_before_4_0 + db = build('3.4') + m = db.get_method(BitClust::MethodSpec.parse('Kernel.#mf')) + assert_equal('.#', m.display_typemark) + assert_equal('Kernel.#mf', m.display_label) + assert_equal('.#mf', m.display_short_label) + assert_equal(['Kernel.#mf', 'Kernel.#mf2'], m.display_title_labels) + end + + def test_module_function_display_switches_to_question_dot_at_4_0 + db = build('4.0') + m = db.get_method(BitClust::MethodSpec.parse('Kernel.#mf')) + assert_equal('?.', m.display_typemark) + assert_equal('Kernel?.mf', m.display_label) + assert_equal('?.mf', m.display_short_label) + assert_equal(['Kernel?.mf', 'Kernel?.mf2'], m.display_title_labels) + end + + # 識別子として使われる label/labels/short_label/title_labels は、 + # 表示版とは無関係に version が 4.0 以降でも従来どおり ".#" のまま + def test_identity_methods_are_unaffected_by_version + db = build('4.0') + m = db.get_method(BitClust::MethodSpec.parse('Kernel.#mf')) + assert_equal('.#', m.typemark) + assert_equal('Kernel.#mf', m.label) + assert_equal('.#mf', m.short_label) + assert_equal(['Kernel.#mf', 'Kernel.#mf2'], m.title_labels) + assert_equal(['Kernel.#mf', 'Kernel.#mf2'], m.labels) + end + + def test_instance_method_and_special_variable_display_are_unaffected_by_version + db = build('4.0') + im = db.get_method(BitClust::MethodSpec.parse('Kernel#im')) + assert_equal('#', im.display_typemark) + assert_equal('Kernel#im', im.display_label) + assert_equal('im', im.display_short_label) + + sv = db.get_method(BitClust::MethodSpec.parse('Kernel$stdout')) + assert_equal('$', sv.display_typemark) + assert_equal('$stdout', sv.display_label) + assert_equal('$stdout', sv.display_short_label) + end +end + # メソッド名別の since/until (bitclust#132 P1)。 # # テストリスト: diff --git a/test/test_nameutils.rb b/test/test_nameutils.rb index c1250e3f..12daf39f 100644 --- a/test/test_nameutils.rb +++ b/test/test_nameutils.rb @@ -306,6 +306,28 @@ def test_typemark2char(data) assert_equal(expected, typemark2char(target)) end + # bitclust#250: Ruby 4.0 以降のドキュメントでは module function の表記を + # 独自の「.#」から「?.」に変える。表示だけの変換であり、識別子(typemark + # そのもの・typemark2char 等)は一切変えないので、対象は '.#' のみ。 + # バージョン比較は文字列比較ではなく Gem::Version で行う(例: "10.0" を + # 文字列比較すると "4.0" より小さく見えてしまう)。 + data("module function before 4.0 stays as-is" => ['.#', '.#', '3.4'], + "module function at exactly 4.0 switches" => ['?.', '.#', '4.0'], + "module function after 4.0 switches" => ['?.', '.#', '4.1'], + "module function 4.0.0 counts as 4.0" => ['?.', '.#', '4.0.0'], + "module function double-digit major (10.0)" => ['?.', '.#', '10.0'], + "module function just under 4.0 stays as-is" => ['.#', '.#', '3.99'], + "module function with nil version stays as-is" => ['.#', '.#', nil], + "module function with empty version stays" => ['.#', '.#', ''], + "singleton method mark is never rewritten" => ['.', '.', '4.0'], + "instance method mark is never rewritten" => ['#', '#', '4.0'], + "constant mark is never rewritten" => ['::', '::', '4.0'], + "special variable mark is never rewritten" => ['$', '$', '4.0']) + def test_display_typemark(data) + expected, mark, version = data + assert_equal(expected, display_typemark(mark, version)) + end + data("s" => [".", "s"], "i" => ["#", "i"], "m" => [".#", "m"], diff --git a/test/test_rdcompiler.rb b/test/test_rdcompiler.rb index 362c66ef..ae074c6d 100644 --- a/test/test_rdcompiler.rb +++ b/test/test_rdcompiler.rb @@ -214,6 +214,47 @@ def test_method assert_compiled_method_source(expected, src) end + # bitclust#250: template.lillia/library だけが compile_method(m, true) で + # 第2引数(@opt)を渡し、見出し先頭に「クラス名+typemark」を付ける。4.0 + # 以降のドキュメントではこの prefix も "?." 表示に切り替わる(識別子である + # typemark ではなく、表示専用の display_typemark を経由する) + def test_method_with_opt_prefixes_class_name_and_display_typemark + klass = Object.new + mock(klass).name { 'Kernel' } + + method_entry = Object.new + mock(method_entry).source { "--- mf\n\nfoo\n" } + mock(method_entry).index_id.any_times { 'dummy' } + mock(method_entry).defined?.any_times { true } + mock(method_entry).id.any_times { 'Kernel/m.mf._builtin' } + mock(method_entry).names.any_times { ['mf'] } + mock(method_entry).since_map.any_times { {} } + mock(method_entry).until_map.any_times { {} } + mock(method_entry).klass.any_times { klass } + mock(method_entry).display_typemark.any_times { '?.' } + + html = @c.compile_method(method_entry, true) + assert_include(html, '<code>Kernel?.mf</code>') + end + + # opt を渡さない通常経路(class/method/function 各テンプレート)は + # klass/display_typemark に一切触れず、prefix なしのまま(回帰防止: + # klass/display_typemark をスタブしていないので、誤って参照すると + # 未定義呼び出しとして失敗する) + def test_method_without_opt_omits_the_class_and_typemark_prefix + method_entry = Object.new + mock(method_entry).source { "--- mf\n\nfoo\n" } + mock(method_entry).index_id.any_times { 'dummy' } + mock(method_entry).defined?.any_times { true } + mock(method_entry).id.any_times { 'Kernel/m.mf._builtin' } + mock(method_entry).names.any_times { ['mf'] } + mock(method_entry).since_map.any_times { {} } + mock(method_entry).until_map.any_times { {} } + + html = @c.compile_method(method_entry) + assert_include(html, '<code>mf</code>') + end + def test_method_with_emlist src = <<'HERE' --- <=> diff --git a/test/test_search_index_generator.rb b/test/test_search_index_generator.rb index 796f1139..a00abb34 100644 --- a/test/test_search_index_generator.rb +++ b/test/test_search_index_generator.rb @@ -69,6 +69,41 @@ def test_module_function_is_class_method assert_equal 'method/-kernel/m/at_exit.html', e[:path] end + # bitclust#250: Ruby 4.0 以降のドキュメントでは module function の表記を + # 独自の「.#」から「?.」に変える(表示のみ)。full_name は検索結果に + # そのまま出る表示ラベルなので変換対象だが、path(識別子)は変えない + def test_module_function_full_name_switches_to_question_dot_at_4_0 + prefix = 'db_si_40' + base = 'tree_si_40' + root = "#{base}/refm/api/src" + FileUtils.mkdir_p("#{root}/_builtin") + File.write("#{root}/LIBRARIES", "_builtin\n") + File.write("#{root}/_builtin.rd", <<~RD) + description + + = module Kernel + description + == Module Functions + --- at_exit{ ... } -> Proc + aaa + RD + db = BitClust::MethodDatabase.new(prefix) + db.init + db.transaction do + [%w[version 4.0], %w[encoding utf-8]].each { |k, v| db.propset(k, v) } + end + db.transaction { db.update_by_stdlibtree(root) } + + index = BitClust::SearchIndexGenerator.new.build_index(db) + e = index.find { |x| x[:path] == 'method/-kernel/m/at_exit.html' } + assert_not_nil e + assert_equal 'at_exit', e[:name] + assert_equal 'class_method', e[:type] + assert_equal 'Kernel?.at_exit', e[:full_name] + ensure + FileUtils.rm_r([prefix, base], :force => true) + end + def test_constant_entry index = @gen.build_index(@db) e = find_entry(index, 'Foo::AAA') diff --git a/test/test_search_screen.rb b/test/test_search_screen.rb new file mode 100644 index 00000000..3c862cc8 --- /dev/null +++ b/test/test_search_screen.rb @@ -0,0 +1,48 @@ +require 'test/unit' +require 'bitclust' +require 'bitclust/screen' + +# Covers data/bitclust/template/search: the dynamic CGI search-results page +# rendered by `bitclust server`'s /search endpoint (SearchScreen). Distinct +# from the static client-side search index (SearchIndexGenerator, covered by +# test_search_index_generator.rb) -- this one builds its "Kernel.#mf"-style +# label directly in the template from klass.name + typemark + signature. +# +# bitclust#250: Ruby 4.0 以降のドキュメントでは module function の表記を +# 独自の「.#」から「?.」に変える(表示のみ、識別子は変えない)。 +class TestSearchScreenModuleFunctionDisplay < Test::Unit::TestCase + SRC = <<'HERE' += module Kernel +description +== Module Functions +--- mf + +説明 +HERE + + def render(version) + _lib, db = BitClust::RRDParser.parse(SRC, 'testlib', {'version' => version}) + entry = db.get_method(BitClust::MethodSpec.parse('Kernel.#mf')) + datadir = File.expand_path('../data/bitclust', __dir__) + manager = BitClust::ScreenManager.new( + :templatedir => "#{datadir}/template", + :catalogdir => "#{datadir}/catalog", + :encoding => 'utf-8', + :default_encoding => 'utf-8', + :base_url => '', + :target_version => version + ) + manager.search_screen([entry], :database => db, :q => 'mf', :elapsed_time => 0.0).body + end + + def test_module_function_shows_dot_hash_before_4_0 + html = render('3.4') + assert_include(html, 'Kernel.#mf') + end + + def test_module_function_switches_to_question_dot_at_4_0 + html = render('4.0') + assert_include(html, 'Kernel?.mf') + assert_not_include(html, 'Kernel.#mf') + end +end From cb770ec930a30bc4d1c196b23fd822c4cdaf4346 Mon Sep 17 00:00:00 2001 From: Kazuhiro NISHIYAMA <zn@mbf.nifty.com> Date: Wed, 22 Jul 2026 13:20:45 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C:=20=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=81=AE=20".#"=20=E3=81=AE=20typo=20=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- lib/bitclust/nameutils.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/bitclust/nameutils.rb b/lib/bitclust/nameutils.rb index 840002c6..218a6922 100644 --- a/lib/bitclust/nameutils.rb +++ b/lib/bitclust/nameutils.rb @@ -208,7 +208,7 @@ def typemark2char(mark) end # bitclust#250: Ruby 4.0 以降のドキュメントでは module function の表記を - # るりま独自の "." から "?." に変える(表示のみ。ソース側は既に "?." + # るりま独自の ".#" から "?." に変える(表示のみ。ソース側は既に "?." # 記法に対応済みで、内部的には従来どおり ".#" に変換される)。 # # これは見た目だけの変換であり、method id・URL・spec 文字列など識別子と