bitclust-core 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -0
  3. data/data/bitclust/catalog/ja_JP.UTF-8 +4 -2
  4. data/data/bitclust/template/function-index +1 -0
  5. data/data/bitclust/template/layout +1 -0
  6. data/data/bitclust/template.offline/function-index +1 -0
  7. data/data/bitclust/template.offline/layout +3 -0
  8. data/lib/bitclust/classentry.rb +1 -1
  9. data/lib/bitclust/docentry.rb +1 -1
  10. data/lib/bitclust/functionentry.rb +1 -1
  11. data/lib/bitclust/irb.rb +68 -0
  12. data/lib/bitclust/markdown_exporter.rb +311 -0
  13. data/lib/bitclust/mdcompiler.rb +15 -1
  14. data/lib/bitclust/mdparser.rb +42 -1
  15. data/lib/bitclust/methoddatabase.rb +4 -0
  16. data/lib/bitclust/methodentry.rb +24 -0
  17. data/lib/bitclust/methodsignature.rb +1 -1
  18. data/lib/bitclust/preprocessor.rb +4 -1
  19. data/lib/bitclust/rbs_overload_matcher.rb +179 -0
  20. data/lib/bitclust/rbs_sig_importer.rb +276 -0
  21. data/lib/bitclust/rbs_signatures.rb +125 -0
  22. data/lib/bitclust/rdcompiler.rb +12 -1
  23. data/lib/bitclust/runner.rb +2 -0
  24. data/lib/bitclust/screen.rb +4 -0
  25. data/lib/bitclust/search_index_generator.rb +28 -10
  26. data/lib/bitclust/searcher.rb +12 -2
  27. data/lib/bitclust/subcommands/rbssig_command.rb +99 -0
  28. data/lib/bitclust/subcommands/setup_command.rb +4 -1
  29. data/lib/bitclust/subcommands/statichtml_command.rb +59 -0
  30. data/lib/bitclust/version.rb +1 -1
  31. data/lib/bitclust-irb.rb +2 -0
  32. data/test/test_irb_plugin.rb +118 -0
  33. data/test/test_link_checker.rb +1 -1
  34. data/test/test_markdown_exporter.rb +260 -0
  35. data/test/test_mdcompiler.rb +47 -5
  36. data/test/test_mdparser.rb +219 -4
  37. data/test/test_methodentry.rb +111 -0
  38. data/test/test_methodsignature.rb +3 -1
  39. data/test/test_preprocessor.rb +26 -0
  40. data/test/test_rbs_overload_matcher.rb +170 -0
  41. data/test/test_rbs_sig_importer.rb +237 -0
  42. data/test/test_rbssig_command.rb +186 -0
  43. data/test/test_rdcompiler.rb +99 -1
  44. data/test/test_search_index_generator.rb +81 -0
  45. data/test/test_setup_command.rb +21 -0
  46. data/test/test_statichtml_command.rb +137 -0
  47. data/theme/default/js/run.js +14 -0
  48. data/theme/default/js/search_init.js +3 -1
  49. data/theme/default/js/search_page.js +7 -1
  50. data/theme/default/js/version_switcher.js +147 -0
  51. data/theme/default/search.css +21 -0
  52. data/theme/default/style.css +83 -7
  53. data/theme/lillia/style.css +26 -0
  54. metadata +35 -1
@@ -3,6 +3,7 @@ require 'bitclust'
3
3
  require 'bitclust/methoddatabase'
4
4
  require 'tmpdir'
5
5
  require 'fileutils'
6
+ require 'json'
6
7
 
7
8
  class TestMethodEntryTitleLabels < Test::Unit::TestCase
8
9
  def test_title_labels_matches_label_when_no_alias
@@ -326,3 +327,113 @@ class TestMethodEntrySinceUntil < Test::Unit::TestCase
326
327
  File.write(path, lines.join)
327
328
  end
328
329
  end
330
+
331
+ # rbs_sig property(RBS シグネチャ表示)。オーバーロード配列
332
+ # ({"segments" => 行, "params"/"arity"/"block" => メタ情報} の配列)を
333
+ # {"overloads": [...]} の JSON 1 行で 'String' 型 property に格納する。
334
+ # JSON は改行を含まず、値中の '=' や ',' も property ファイル
335
+ # (key=value 1 行形式)で安全。
336
+ #
337
+ # テストリスト:
338
+ # [x] 既定は nil で rbs_signature_overloads も nil
339
+ # [x] JSON を入れて save → reload で round-trip する
340
+ # [x] 旧形式(セグメント行列そのものの配列)は segments だけの
341
+ # オーバーロード列に読み替える(#322 以前の DB)
342
+ # [x] property 行が無い古い DB でも nil(後方互換)
343
+ # [x] 空文字列(シグネチャ無しで save された値)も nil 扱い
344
+ # [x] 壊れた JSON は nil 扱い(描画を落とさない)
345
+ class TestMethodEntryRbsSig < Test::Unit::TestCase
346
+ OVERLOADS = [
347
+ {'segments' => [['t', '('], ['c', 'Integer'], ['t', ') -> '], ['c', 'String']],
348
+ 'params' => ['base'], 'arity' => [1, 1]},
349
+ {'segments' => [['t', '() -> void']],
350
+ 'params' => [], 'arity' => [0, 0], 'block' => 'req'},
351
+ ]
352
+
353
+ def setup
354
+ @dir = Dir.mktmpdir
355
+ @db = BitClust::MethodDatabase.new(@dir)
356
+ @db.init
357
+ @db.transaction do
358
+ @db.propset('version', '4.0')
359
+ @db.propset('encoding', 'utf-8')
360
+ end
361
+ end
362
+
363
+ def teardown
364
+ FileUtils.rm_rf(@dir)
365
+ end
366
+
367
+ # 未保存エントリの String 型 property は "(uninitialized)" センチネル
368
+ # なので、rbs_sig そのものではなく overloads が nil であることを見る
369
+ def test_default_has_no_overloads
370
+ m = build_entry('sig')
371
+ assert_nil m.rbs_signature_overloads
372
+ end
373
+
374
+ def test_roundtrip
375
+ m = build_entry('sig')
376
+ m.rbs_sig = JSON.generate({'overloads' => OVERLOADS})
377
+ m.save
378
+
379
+ reloaded = reload_entry(m.id)
380
+ assert_equal(OVERLOADS, reloaded.rbs_signature_overloads)
381
+ end
382
+
383
+ def test_old_array_format_is_read_as_segments_only
384
+ m = build_entry('sig')
385
+ m.rbs_sig = JSON.generate(OVERLOADS.map {|o| o['segments'] })
386
+ m.save
387
+
388
+ reloaded = reload_entry(m.id)
389
+ assert_equal(OVERLOADS.map {|o| {'segments' => o['segments']} },
390
+ reloaded.rbs_signature_overloads)
391
+ end
392
+
393
+ def test_missing_property_is_backward_compatible
394
+ m = build_entry('sig')
395
+ m.save
396
+ strip_property_line(m.id, 'rbs_sig')
397
+
398
+ reloaded = reload_entry(m.id)
399
+ assert_nil reloaded.rbs_sig
400
+ assert_nil reloaded.rbs_signature_overloads
401
+ end
402
+
403
+ def test_saved_without_signature_is_treated_as_absent
404
+ m = build_entry('sig')
405
+ m.save
406
+
407
+ reloaded = reload_entry(m.id)
408
+ assert_nil reloaded.rbs_signature_overloads
409
+ end
410
+
411
+ def test_broken_json_is_treated_as_absent
412
+ m = build_entry('sig')
413
+ m.rbs_sig = '{broken'
414
+ assert_nil m.rbs_signature_overloads
415
+ end
416
+
417
+ private
418
+
419
+ def build_entry(name)
420
+ id = BitClust::NameUtils.build_method_id('_builtin', 'Foo', :instance_method, name)
421
+ m = BitClust::MethodEntry.new(@db, id)
422
+ m.names = [name]
423
+ m.visibility = :public
424
+ m.kind = :defined
425
+ m.source = "説明\n"
426
+ m
427
+ end
428
+
429
+ def reload_entry(id)
430
+ fresh_db = BitClust::MethodDatabase.new(@dir)
431
+ BitClust::MethodEntry.new(fresh_db, id)
432
+ end
433
+
434
+ def strip_property_line(id, key)
435
+ path = File.join(@dir, BitClust::NameUtils.encodeid("method/#{id}"))
436
+ lines = File.readlines(path).reject {|l| l.start_with?("#{key}=") }
437
+ File.write(path, lines.join)
438
+ end
439
+ end
@@ -6,7 +6,9 @@ class TestMethodSignature < Test::Unit::TestCase
6
6
  data("special var" => ["$_ -> String | nil", "--- $_ -> String | nil"],
7
7
  "backquote" => ["`command` -> String", "--- `(command) -> String"],
8
8
  "aset" => ["self[key] = value", "--- []=(key, value)"],
9
- "aset splat" => ["[]=(*idxary)", "--- []=(*idxary)"])
9
+ "aset splat" => ["[]=(*idxary)", "--- []=(*idxary)"],
10
+ "self prefix" => ["build(x) -> Foo", "--- self.build(x) -> Foo"],
11
+ "class prefix" => ["build(x) -> Foo", "--- Foo.build(x) -> Foo"])
10
12
  def test_friendlyname(data)
11
13
  friendly_string, method_signature = data
12
14
  assert_equal(friendly_string, BitClust::MethodSignature.parse(method_signature).friendly_string)
@@ -516,6 +516,32 @@ HERE
516
516
  assert_equal "", process(src, '4.0')
517
517
  end
518
518
 
519
+ def test_single_version
520
+ # 単一版のみの省略記法(#%if (version == "V") と同義)
521
+ src = gated('#%version 3.1')
522
+ assert_equal "out\n", process(src, '3.0')
523
+ assert_equal "in\n", process(src, '3.1')
524
+ assert_equal "out\n", process(src, '3.2')
525
+ end
526
+
527
+ def test_single_version_quoted
528
+ src = gated('#%version "3.1"')
529
+ assert_equal "in\n", process(src, '3.1')
530
+ assert_equal "out\n", process(src, '3.2')
531
+ end
532
+
533
+ def test_single_version_old_prefix
534
+ src = gated("\#@version 3.1")
535
+ assert_equal "in\n", process(src, '3.1')
536
+ end
537
+
538
+ def test_single_version_teeny
539
+ # 版名は文字列一致(teeny 付き版名は teeny まで一致したときのみ)
540
+ src = gated('#%version 2.7.0')
541
+ assert_equal "in\n", process(src, '2.7.0')
542
+ assert_equal "out\n", process(src, '2.7')
543
+ end
544
+
519
545
  def test_two_dot_range_is_error
520
546
  # .. (終端を含む Range)は誤解を招くため受け付けない(#285)
521
547
  assert_raise(BitClust::ParseError) { process(gated('#%version 3.1..3.4'), '3.2') }
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+ require 'test/unit'
3
+ require 'bitclust/rbs_overload_matcher'
4
+
5
+ # RbsOverloadMatcher(RBS シグネチャ表示)。引数パターンごとに説明チャンクが
6
+ # 分かれているメソッド(Array.new / Array#[] 等)で、RBS の各オーバーロードを
7
+ # 「ブロック有無 → 引数名の一致 → アリティ範囲の重なり」のスコアで最も近い
8
+ # チャンクへ振り分ける。RBS の引数名は rdoc の call-seq 由来で rurema の
9
+ # 引数名とよく一致するのが根拠。確実な対応が取れない場合は先頭チャンクに
10
+ # 寄せる(重複表示はしない)。
11
+ #
12
+ # オーバーロードは rbs_sig property の JSON をそのまま(文字列キーの Hash)
13
+ # 受け取る。チャンクは正規化済みシグネチャ行("--- " 始まり)の配列の配列。
14
+ #
15
+ # テストリスト:
16
+ # [x] チャンクが 1 つなら全オーバーロードをそこへ(スコア不要)
17
+ # [x] メタの無いオーバーロード(旧形式・attr・(?))は先頭チャンクへ
18
+ # [x] Array#[] 型: 引数名 range / start+length で振り分け・名前が合わない
19
+ # ものはアリティ同点 → 先頭チャンク
20
+ # [x] Array.new 型: () はアリティで・(ary) は名前で・ブロック付きは
21
+ # ブロックチャンクへ
22
+ # [x] ブロック必須オーバーロードはブロック無しチャンクより有りを選ぶ
23
+ # [x] チャンク側の引数解析: 省略可能引数・*rest・キーワード・&block・
24
+ # 引数リスト無し
25
+ # [x] 解析できないシグネチャ行だけのチャンクには振り分けない
26
+ class TestRbsOverloadMatcher < Test::Unit::TestCase
27
+ M = BitClust::RbsOverloadMatcher
28
+
29
+ def test_single_chunk_takes_everything
30
+ overloads = [
31
+ {'segments' => [], 'params' => ['x'], 'arity' => [1, 1]},
32
+ {'segments' => [], 'params' => [], 'arity' => [0, 0], 'block' => 'req'},
33
+ ]
34
+ assert_equal [[0, 1]], M.assign(overloads, [['--- foo(x) -> nil']])
35
+ end
36
+
37
+ def test_overload_without_meta_goes_to_first_chunk
38
+ overloads = [{'segments' => []}]
39
+ chunks = [['--- foo(x) -> nil'], ['--- foo(x, y) -> nil']]
40
+ assert_equal [[0], []], M.assign(overloads, chunks)
41
+ end
42
+
43
+ # Array#[] の実データ(rbs v3.10.0)。(int index) は nth/range どちらとも
44
+ # 名前が合わずアリティも同点 → 先頭の nth チャンクへ。range と
45
+ # start+length は引数名で決まる
46
+ def test_aref_like_assignment
47
+ chunks = [
48
+ ['--- [](nth) -> object | nil'],
49
+ ['--- [](range) -> Array | nil'],
50
+ ['--- [](start, length) -> Array | nil'],
51
+ ]
52
+ overloads = [
53
+ {'segments' => [], 'params' => ['index'], 'arity' => [1, 1]},
54
+ {'segments' => [], 'params' => ['start', 'length'], 'arity' => [2, 2]},
55
+ {'segments' => [], 'params' => ['range'], 'arity' => [1, 1]},
56
+ ]
57
+ assert_equal [[0], [2], [1]], M.assign(overloads, chunks)
58
+ end
59
+
60
+ # Array.new の実データ(rbs v3.10.0)。() はアリティ 0 を含む先頭チャンク
61
+ # だけに重なる。(ary) と (size, ?val) は引数名で分かれ、ブロック付きは
62
+ # ブロックチャンクへ
63
+ def test_new_like_assignment
64
+ chunks = [
65
+ ['--- new(size = 0, val = nil) -> Array'],
66
+ ['--- new(ary) -> Array'],
67
+ ['--- new(size) {|index| ... } -> Array'],
68
+ ]
69
+ overloads = [
70
+ {'segments' => [], 'params' => [], 'arity' => [0, 0]},
71
+ {'segments' => [], 'params' => ['ary'], 'arity' => [1, 1]},
72
+ {'segments' => [], 'params' => ['size', 'val'], 'arity' => [1, 2]},
73
+ {'segments' => [], 'params' => ['size'], 'arity' => [1, 1], 'block' => 'req'},
74
+ ]
75
+ assert_equal [[0, 2], [1], [3]], M.assign(overloads, chunks)
76
+ end
77
+
78
+ def test_required_block_prefers_block_chunk
79
+ chunks = [
80
+ ['--- each -> Enumerator'],
81
+ ['--- each {|item| ... } -> self'],
82
+ ]
83
+ overloads = [
84
+ {'segments' => [], 'params' => [], 'arity' => [0, 0]},
85
+ {'segments' => [], 'params' => [], 'arity' => [0, 0], 'block' => 'req'},
86
+ ]
87
+ assert_equal [[0], [1]], M.assign(overloads, chunks)
88
+ end
89
+
90
+ # 省略可能ブロック(?{ ... })はどちらのチャンクとも矛盾しない。
91
+ # 引数名の一致で決まる
92
+ def test_optional_block_is_neutral
93
+ chunks = [
94
+ ['--- open(path) -> IO'],
95
+ ['--- open(fd) {|io| ... } -> object'],
96
+ ]
97
+ overloads = [
98
+ {'segments' => [], 'params' => ['fd'], 'arity' => [1, 1], 'block' => 'opt'},
99
+ ]
100
+ assert_equal [[], [0]], M.assign(overloads, chunks)
101
+ end
102
+
103
+ # チャンク側の引数リスト解析。省略可能引数で最小アリティが下がり、
104
+ # *rest で上限が消え、キーワード名も名前照合に使われる
105
+ def test_chunk_parsing_of_optional_rest_and_keywords
106
+ chunks = [
107
+ ['--- foo(a, b = 1, *rest, key: 0) -> nil'],
108
+ ['--- foo(x, y) -> nil'],
109
+ ]
110
+ overloads = [
111
+ {'segments' => [], 'params' => ['key'], 'arity' => [5, 5]},
112
+ {'segments' => [], 'params' => ['x', 'y'], 'arity' => [2, 2]},
113
+ ]
114
+ assert_equal [[0], [1]], M.assign(overloads, chunks)
115
+ end
116
+
117
+ # &block 引数はブロック有りとして扱う
118
+ def test_chunk_block_argument_counts_as_block
119
+ chunks = [
120
+ ['--- foo -> Enumerator'],
121
+ ['--- foo(&block) -> object'],
122
+ ]
123
+ overloads = [
124
+ {'segments' => [], 'params' => [], 'arity' => [0, 0], 'block' => 'req'},
125
+ ]
126
+ assert_equal [[], [0]], M.assign(overloads, chunks)
127
+ end
128
+
129
+ # 引数リストの無いシグネチャはアリティ 0 扱い
130
+ def test_chunk_without_params_is_zero_arity
131
+ chunks = [
132
+ ['--- size -> Integer'],
133
+ ['--- size(n) -> Integer'],
134
+ ]
135
+ overloads = [
136
+ {'segments' => [], 'params' => [], 'arity' => [0, 0]},
137
+ {'segments' => [], 'params' => ['n'], 'arity' => [1, 1]},
138
+ ]
139
+ assert_equal [[0], [1]], M.assign(overloads, chunks)
140
+ end
141
+
142
+ # 同一チャンクに複数シグネチャ行(別名など)がある場合は特徴を合算する
143
+ def test_multiple_signature_lines_in_one_chunk
144
+ chunks = [
145
+ ['--- collect {|item| ... } -> [object]', '--- map {|item| ... } -> [object]'],
146
+ ['--- collect -> Enumerator', '--- map -> Enumerator'],
147
+ ]
148
+ overloads = [
149
+ {'segments' => [], 'params' => [], 'arity' => [0, 0], 'block' => 'req'},
150
+ {'segments' => [], 'params' => [], 'arity' => [0, 0]},
151
+ ]
152
+ assert_equal [[0], [1]], M.assign(overloads, chunks)
153
+ end
154
+
155
+ # 解析できない行だけのチャンクは振り分け先にならない
156
+ def test_unparseable_chunk_is_skipped
157
+ chunks = [
158
+ ['?????'],
159
+ ['--- foo(x) -> nil'],
160
+ ]
161
+ overloads = [
162
+ {'segments' => [], 'params' => [], 'arity' => [0, 0]},
163
+ ]
164
+ assert_equal [[], [0]], M.assign(overloads, chunks)
165
+ end
166
+
167
+ def test_empty_chunks_returns_empty
168
+ assert_equal [], M.assign([{'segments' => []}], [])
169
+ end
170
+ end
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+ require 'test/unit'
3
+ require 'bitclust'
4
+ require 'bitclust/methoddatabase'
5
+ require 'bitclust/rbs_sig_importer'
6
+ require 'tmpdir'
7
+ require 'fileutils'
8
+ require 'json'
9
+
10
+ # RbsSigImporter(RBS シグネチャ表示)。.rbs 群から
11
+ # "Class#method"/"Class.method" キー → オーバーロード配列の対応を作り、
12
+ # MethodEntry の rbs_sig property に書き込む。
13
+ #
14
+ # オーバーロード = {"segments" => 行, "params" => 引数名, "arity" =>
15
+ # [最小, 最大|nil], "block" => "req"|"opt"} の Hash(segments 以外は
16
+ # チャンク振り分け=RbsOverloadMatcher 用のメタ情報で、無い形式もある)。
17
+ # 行は [種別, テキスト] の配列で、種別 "t" は素のテキスト、"c" は
18
+ # クラス/モジュール名(描画側が DB に存在すればリンク化する)。行の
19
+ # テキストを連結すると元の型シグネチャ文字列に戻る(往復可能)。
20
+ #
21
+ # テストリスト:
22
+ # [x] instance/singleton メソッドが # / . キーになる
23
+ # [x] overload はオーバーロードごとに分かれ、テキスト連結で元に戻る
24
+ # [x] 型名は ["c", name] セグメントになる(引数・戻り値・ジェネリクスの中も)
25
+ # [x] 名前空間付き型名は :: が直前のテキスト側に残る
26
+ # [x] メタ情報: 引数名(位置+キーワード)・アリティ範囲・ブロック有無
27
+ # [x] メタ情報: 引数の無いオーバーロードは params=[]・arity=[0,0]
28
+ # [x] def self?. は # と . の両キーに登録される
29
+ # [x] alias メンバーは元メソッドのシグネチャを共有する
30
+ # [x] attr_reader/attr_writer は name / name= キーになり、型だけの
31
+ # オーバーロード(メタ無し)になる
32
+ # [x] lookup: typechar i/s/m で引ける(m は # → . の順)
33
+ # [x] lookup: new は Class.new → Class#initialize の順でフォールバック
34
+ # [x] lookup: 別名リストの後ろの名前でもヒットする
35
+ # [x] lookup: 対応が無ければ nil(定数・特殊変数も nil)
36
+ # [x] apply: DB のエントリに rbs_sig(overloads 形式の JSON)が書き込まれる
37
+ # [x] apply: 対応が無いメソッドには書き込まれない
38
+ # [x] apply: 2 回目は無変更(entries_updated=0)
39
+ class TestRbsSigImporter < Test::Unit::TestCase
40
+ def setup
41
+ @tmpdir = Dir.mktmpdir
42
+ @sig_dir = File.join(@tmpdir, 'sig')
43
+ FileUtils.mkdir_p(@sig_dir)
44
+ File.write(File.join(@sig_dir, 'foo.rbs'), <<~RBS)
45
+ class Foo
46
+ def initialize: (String) -> void
47
+ def bar: (Integer base) -> String
48
+ | (::Enumerator::Lazy) -> Array[Integer]
49
+ def self.build: () -> Foo
50
+ def self?.mf: () -> bool
51
+ alias car bar
52
+ attr_reader label: String
53
+ attr_writer width: Integer
54
+ def pairs: (hash[Symbol, String]) -> void
55
+ def meta: (Integer x, ?String y, *Integer rest, size: Integer, ?name: String) { (Integer) -> void } -> void
56
+ | (Integer a, Integer b) ?{ () -> void } -> void
57
+ end
58
+ RBS
59
+ @importer = BitClust::RbsSigImporter.new(sig_dirs: [@sig_dir])
60
+ end
61
+
62
+ def teardown
63
+ FileUtils.rm_rf(@tmpdir)
64
+ end
65
+
66
+ def test_instance_and_singleton_methods_are_keyed
67
+ sigs = @importer.signatures
68
+ assert sigs.key?('Foo#bar')
69
+ assert sigs.key?('Foo#initialize')
70
+ assert sigs.key?('Foo.build')
71
+ assert_false sigs.key?('Foo#build')
72
+ end
73
+
74
+ def test_overload_lines_roundtrip_to_original_signature
75
+ overloads = @importer.signatures['Foo#bar']
76
+ assert_equal 2, overloads.size
77
+ assert_equal '(Integer base) -> String', join_text(overloads[0])
78
+ assert_equal '(::Enumerator::Lazy) -> Array[Integer]', join_text(overloads[1])
79
+ end
80
+
81
+ def test_type_names_become_class_segments
82
+ overloads = @importer.signatures['Foo#bar']
83
+ assert_equal(
84
+ [['t', '('], ['c', 'Integer'], ['t', ' base) -> '], ['c', 'String']],
85
+ overloads[0]['segments'])
86
+ end
87
+
88
+ def test_namespace_prefix_stays_in_text_segment
89
+ overloads = @importer.signatures['Foo#bar']
90
+ assert_equal(
91
+ [['t', '(::'], ['c', 'Enumerator::Lazy'], ['t', ') -> '],
92
+ ['c', 'Array'], ['t', '['], ['c', 'Integer'], ['t', ']']],
93
+ overloads[1]['segments'])
94
+ end
95
+
96
+ # 小文字のエイリアス型(hash 等)自体はテキストのままだが、その
97
+ # ジェネリクス引数の中のクラス名はリンク化候補にする
98
+ def test_alias_type_args_become_class_segments
99
+ overloads = @importer.signatures['Foo#pairs']
100
+ assert_equal(
101
+ [['t', '(hash['], ['c', 'Symbol'], ['t', ', '], ['c', 'String'],
102
+ ['t', ']) -> void']],
103
+ overloads[0]['segments'])
104
+ end
105
+
106
+ # チャンク振り分け用のメタ情報。引数名は位置引数(rest 含む)+
107
+ # キーワード名、アリティは位置引数の [最小, 最大](rest があれば nil)、
108
+ # block は必須 "req" / 省略可能 "opt"(無ければキー自体無し)
109
+ def test_overload_meta
110
+ overloads = @importer.signatures['Foo#meta']
111
+ assert_equal %w[x y rest size name], overloads[0]['params']
112
+ assert_equal [1, nil], overloads[0]['arity']
113
+ assert_equal 'req', overloads[0]['block']
114
+ assert_equal %w[a b], overloads[1]['params']
115
+ assert_equal [2, 2], overloads[1]['arity']
116
+ assert_equal 'opt', overloads[1]['block']
117
+ end
118
+
119
+ def test_overload_meta_without_params
120
+ overloads = @importer.signatures['Foo.build']
121
+ assert_equal [], overloads[0]['params']
122
+ assert_equal [0, 0], overloads[0]['arity']
123
+ assert_nil overloads[0]['block']
124
+ # 名前の無い位置引数はアリティにだけ数える
125
+ assert_equal [], @importer.signatures['Foo#initialize'][0]['params']
126
+ assert_equal [1, 1], @importer.signatures['Foo#initialize'][0]['arity']
127
+ end
128
+
129
+ def test_self_question_registers_both_keys
130
+ sigs = @importer.signatures
131
+ assert_equal sigs['Foo#mf'], sigs['Foo.mf']
132
+ assert_equal '() -> bool', join_text(sigs['Foo#mf'][0])
133
+ end
134
+
135
+ def test_alias_member_shares_signature
136
+ sigs = @importer.signatures
137
+ assert_equal sigs['Foo#bar'], sigs['Foo#car']
138
+ end
139
+
140
+ def test_attr_members
141
+ sigs = @importer.signatures
142
+ assert_equal 'String', join_text(sigs['Foo#label'][0])
143
+ assert_equal 'Integer', join_text(sigs['Foo#width='][0])
144
+ assert_equal({'segments' => [['c', 'String']]}, sigs['Foo#label'][0])
145
+ end
146
+
147
+ def test_lookup_by_typechar
148
+ sigs = @importer.signatures
149
+ assert_equal sigs['Foo#bar'], @importer.lookup('Foo', 'i', ['bar'])
150
+ assert_equal sigs['Foo.build'], @importer.lookup('Foo', 's', ['build'])
151
+ assert_equal sigs['Foo#mf'], @importer.lookup('Foo', 'm', ['mf'])
152
+ end
153
+
154
+ def test_lookup_new_falls_back_to_initialize
155
+ assert_equal @importer.signatures['Foo#initialize'],
156
+ @importer.lookup('Foo', 's', ['new'])
157
+ end
158
+
159
+ def test_lookup_tries_every_alias_name
160
+ assert_equal @importer.signatures['Foo#bar'],
161
+ @importer.lookup('Foo', 'i', ['zzz', 'bar'])
162
+ end
163
+
164
+ def test_lookup_misses_return_nil
165
+ assert_nil @importer.lookup('Foo', 'i', ['nope'])
166
+ assert_nil @importer.lookup('Bar', 'i', ['bar'])
167
+ assert_nil @importer.lookup('Foo', 'c', ['BAR'])
168
+ assert_nil @importer.lookup('Foo', 'v', ['$bar'])
169
+ end
170
+
171
+ def test_apply_writes_rbs_sig_property
172
+ prefix = build_db('4.0')
173
+ stats = @importer.apply(BitClust::MethodDatabase.new(prefix))
174
+
175
+ bar = find_entry(prefix, 'bar')
176
+ assert_equal({'overloads' => @importer.signatures['Foo#bar']},
177
+ JSON.parse(bar.rbs_sig))
178
+ assert_equal 1, stats[:entries_updated]
179
+ assert_equal 1, stats[:sigs_matched]
180
+ assert_equal 1, stats[:methods_missed]
181
+ end
182
+
183
+ def test_apply_leaves_unmatched_methods_untouched
184
+ prefix = build_db('4.0')
185
+ @importer.apply(BitClust::MethodDatabase.new(prefix))
186
+ assert_nil find_entry(prefix, 'nope').rbs_signature_overloads
187
+ end
188
+
189
+ def test_apply_twice_is_idempotent
190
+ prefix = build_db('4.0')
191
+ @importer.apply(BitClust::MethodDatabase.new(prefix))
192
+ stats = @importer.apply(BitClust::MethodDatabase.new(prefix))
193
+ assert_equal 0, stats[:entries_updated]
194
+ assert_equal 1, stats[:sigs_matched]
195
+ end
196
+
197
+ private
198
+
199
+ def join_text(overload)
200
+ overload['segments'].map {|_kind, text| text }.join
201
+ end
202
+
203
+ def build_db(version)
204
+ root = "#{@tmpdir}/tree-#{version}/refm/api/src"
205
+ FileUtils.mkdir_p("#{root}/_builtin")
206
+ File.write("#{root}/LIBRARIES", "_builtin\n")
207
+ File.write("#{root}/_builtin.rd", <<~RD)
208
+ description
209
+
210
+ = class Foo < Object
211
+ == Instance Methods
212
+ --- bar(base) -> String
213
+
214
+ 説明
215
+
216
+ --- nope -> nil
217
+
218
+ 説明
219
+ RD
220
+ prefix = "#{@tmpdir}/db-#{version}"
221
+ db = BitClust::MethodDatabase.new(prefix)
222
+ db.init
223
+ db.transaction do
224
+ db.propset('version', version)
225
+ db.propset('encoding', 'utf-8')
226
+ end
227
+ db.transaction do
228
+ db.update_by_stdlibtree(root)
229
+ end
230
+ prefix
231
+ end
232
+
233
+ def find_entry(prefix, name)
234
+ db = BitClust::MethodDatabase.new(prefix)
235
+ db.get_class('Foo').entries.find {|m| m.name?(name) }
236
+ end
237
+ end