bitclust-core 1.6.1 → 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 (53) 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/methodentry.rb +24 -0
  16. data/lib/bitclust/methodsignature.rb +1 -1
  17. data/lib/bitclust/preprocessor.rb +4 -1
  18. data/lib/bitclust/rbs_overload_matcher.rb +179 -0
  19. data/lib/bitclust/rbs_sig_importer.rb +276 -0
  20. data/lib/bitclust/rbs_signatures.rb +125 -0
  21. data/lib/bitclust/rdcompiler.rb +12 -1
  22. data/lib/bitclust/runner.rb +2 -0
  23. data/lib/bitclust/screen.rb +4 -0
  24. data/lib/bitclust/search_index_generator.rb +28 -10
  25. data/lib/bitclust/searcher.rb +12 -2
  26. data/lib/bitclust/subcommands/rbssig_command.rb +99 -0
  27. data/lib/bitclust/subcommands/setup_command.rb +4 -1
  28. data/lib/bitclust/subcommands/statichtml_command.rb +59 -0
  29. data/lib/bitclust/version.rb +1 -1
  30. data/lib/bitclust-irb.rb +2 -0
  31. data/test/test_irb_plugin.rb +118 -0
  32. data/test/test_link_checker.rb +1 -1
  33. data/test/test_markdown_exporter.rb +260 -0
  34. data/test/test_mdcompiler.rb +47 -5
  35. data/test/test_mdparser.rb +184 -4
  36. data/test/test_methodentry.rb +111 -0
  37. data/test/test_methodsignature.rb +3 -1
  38. data/test/test_preprocessor.rb +26 -0
  39. data/test/test_rbs_overload_matcher.rb +170 -0
  40. data/test/test_rbs_sig_importer.rb +237 -0
  41. data/test/test_rbssig_command.rb +186 -0
  42. data/test/test_rdcompiler.rb +99 -1
  43. data/test/test_search_index_generator.rb +81 -0
  44. data/test/test_setup_command.rb +21 -0
  45. data/test/test_statichtml_command.rb +137 -0
  46. data/theme/default/js/run.js +14 -0
  47. data/theme/default/js/search_init.js +3 -1
  48. data/theme/default/js/search_page.js +7 -1
  49. data/theme/default/js/version_switcher.js +147 -0
  50. data/theme/default/search.css +21 -0
  51. data/theme/default/style.css +83 -7
  52. data/theme/lillia/style.css +26 -0
  53. metadata +35 -1
@@ -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
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+ require 'test/unit'
3
+ require 'bitclust'
4
+ require 'bitclust/methoddatabase'
5
+ require 'bitclust/subcommands/rbssig_command'
6
+ require 'stringio'
7
+ require 'tmpdir'
8
+ require 'fileutils'
9
+ require 'json'
10
+
11
+ # rbssig サブコマンド(RBS シグネチャ表示)。
12
+ #
13
+ # bitclust rbssig --sig-dir=DIR <dbpath>
14
+ # bitclust rbssig --sig-root=RBS_CHECKOUT <dbpath>
15
+ #
16
+ # RBS シグネチャは 4.0 以降のドキュメント専用なので、DB の version が
17
+ # 4.0 未満ならエラーにする(比較は display_typemark と同じく Gem::Version。
18
+ # 文字列比較だと "10.0" が "4.0" より小さく見える)。
19
+ #
20
+ # テストリスト:
21
+ # [x] グローバル --database 不要(needs_database? が false)
22
+ # [x] --sig-root/--sig-dir がどちらも無いとエラー
23
+ # [x] dbpath がちょうど 1 個でないとエラー
24
+ # [x] DB の version が 4.0 未満だとエラー
25
+ # [x] version 10.0 は 4.0 以降として通る(Gem::Version 比較)
26
+ # [x] 4.0 の DB に rbs_sig が書き込まれ、統計行が出力される
27
+ # [x] --dry-run は統計だけ表示して実 DB には書き込まない
28
+ # [x] --sig-root は core/ と stdlib/ を持つチェックアウトを読める
29
+ # [x] Runner に登録されている(bitclust rbssig として呼べる)
30
+ class TestRbssigCommand < Test::Unit::TestCase
31
+ def setup
32
+ @tmpdir = Dir.mktmpdir
33
+ @sig_dir = File.join(@tmpdir, 'sig')
34
+ FileUtils.mkdir_p(@sig_dir)
35
+ File.write(File.join(@sig_dir, 'foo.rbs'), <<~RBS)
36
+ class Foo
37
+ def bar: (Integer) -> String
38
+ end
39
+ RBS
40
+ end
41
+
42
+ def teardown
43
+ FileUtils.rm_rf(@tmpdir)
44
+ end
45
+
46
+ def test_needs_no_global_database_option
47
+ cmd = BitClust::Subcommands::RbssigCommand.new
48
+ assert_false cmd.needs_database?
49
+ end
50
+
51
+ def test_no_sig_source_is_an_error
52
+ db = build_db('4.0')
53
+ assert_command_error([db])
54
+ end
55
+
56
+ def test_requires_exactly_one_dbpath
57
+ db = build_db('4.0')
58
+ assert_command_error(["--sig-dir=#{@sig_dir}"])
59
+ assert_command_error(["--sig-dir=#{@sig_dir}", db, db])
60
+ end
61
+
62
+ def test_old_database_version_is_an_error
63
+ db = build_db('3.4')
64
+ assert_command_error(["--sig-dir=#{@sig_dir}", db])
65
+ end
66
+
67
+ def test_version_10_passes_the_gate
68
+ db = build_db('10.0')
69
+ out = run_command(["--sig-dir=#{@sig_dir}", db])
70
+ assert_match(/version 10\.0/, out)
71
+ assert_equal [['t', '('], ['c', 'Integer'], ['t', ') -> '], ['c', 'String']],
72
+ find_entry(db, 'bar').rbs_signature_overloads[0]['segments']
73
+ end
74
+
75
+ def test_fills_rbs_sig_and_prints_stats
76
+ db = build_db('4.0')
77
+ out = run_command(["--sig-dir=#{@sig_dir}", db])
78
+ assert_equal '(Integer) -> String',
79
+ find_entry(db, 'bar').rbs_signature_overloads[0]['segments'].map {|_k, t| t }.join
80
+ assert_nil find_entry(db, 'nope').rbs_signature_overloads
81
+ assert_match(
82
+ /\Adb-4\.0 \(version 4\.0\): entries_updated=1 sigs_matched=1 methods_missed=1\n\z/,
83
+ out)
84
+ end
85
+
86
+ def test_dry_run_leaves_database_untouched
87
+ db = build_db('4.0')
88
+ out = run_command(["--sig-dir=#{@sig_dir}", db, '--dry-run'])
89
+ assert_nil find_entry(db, 'bar').rbs_signature_overloads
90
+ assert_match(/entries_updated=1/, out)
91
+ end
92
+
93
+ def test_sig_root_reads_core_and_stdlib_layout
94
+ root = File.join(@tmpdir, 'rbs-checkout')
95
+ FileUtils.mkdir_p("#{root}/core")
96
+ File.write("#{root}/core/foo.rbs", <<~RBS)
97
+ class Foo
98
+ def bar: (String) -> Integer
99
+ end
100
+ RBS
101
+ # core_root を与えると rbs は stringio を暗黙に要求するので、
102
+ # 本物のチェックアウトと同じく stdlib/ も置く
103
+ FileUtils.mkdir_p("#{root}/stdlib/stringio/0")
104
+ File.write("#{root}/stdlib/stringio/0/stringio.rbs", "class StringIO\nend\n")
105
+
106
+ db = build_db('4.0')
107
+ run_command(["--sig-root=#{root}", db])
108
+ assert_equal '(String) -> Integer',
109
+ find_entry(db, 'bar').rbs_signature_overloads[0]['segments'].map {|_k, t| t }.join
110
+ end
111
+
112
+ def test_registered_in_runner
113
+ require 'bitclust/runner'
114
+ runner = BitClust::Runner.new
115
+ runner.prepare
116
+ subcommands = runner.instance_variable_get(:@subcommands)
117
+ assert_kind_of BitClust::Subcommands::RbssigCommand, subcommands['rbssig']
118
+ end
119
+
120
+ private
121
+
122
+ def build_db(version)
123
+ root = "#{@tmpdir}/tree-#{version}/refm/api/src"
124
+ FileUtils.mkdir_p("#{root}/_builtin")
125
+ File.write("#{root}/LIBRARIES", "_builtin\n")
126
+ File.write("#{root}/_builtin.rd", <<~RD)
127
+ description
128
+
129
+ = class Foo < Object
130
+ == Instance Methods
131
+ --- bar(val) -> String
132
+
133
+ 説明
134
+
135
+ --- nope -> nil
136
+
137
+ 説明
138
+ RD
139
+ prefix = "#{@tmpdir}/db-#{version}"
140
+ db = BitClust::MethodDatabase.new(prefix)
141
+ db.init
142
+ db.transaction do
143
+ db.propset('version', version)
144
+ db.propset('encoding', 'utf-8')
145
+ end
146
+ db.transaction do
147
+ db.update_by_stdlibtree(root)
148
+ end
149
+ prefix
150
+ end
151
+
152
+ def find_entry(prefix, name)
153
+ db = BitClust::MethodDatabase.new(prefix)
154
+ db.get_class('Foo').entries.find {|m| m.name?(name) }
155
+ end
156
+
157
+ def run_command(argv)
158
+ cmd = BitClust::Subcommands::RbssigCommand.new
159
+ cmd.parse(argv)
160
+ out = StringIO.new
161
+ orig_stdout = $stdout
162
+ $stdout = out
163
+ begin
164
+ cmd.exec(argv, { prefix: nil, capi: false })
165
+ ensure
166
+ $stdout = orig_stdout
167
+ end
168
+ out.string
169
+ end
170
+
171
+ def assert_command_error(argv)
172
+ cmd = BitClust::Subcommands::RbssigCommand.new
173
+ cmd.parse(argv)
174
+ assert_raise(SystemExit) do
175
+ capture_stderr { cmd.exec(argv, { prefix: nil, capi: false }) }
176
+ end
177
+ end
178
+
179
+ def capture_stderr
180
+ orig = $stderr
181
+ $stderr = StringIO.new
182
+ yield
183
+ ensure
184
+ $stderr = orig
185
+ end
186
+ end