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.
- checksums.yaml +4 -4
- data/Rakefile +1 -0
- data/data/bitclust/catalog/ja_JP.UTF-8 +4 -2
- data/data/bitclust/template/function-index +1 -0
- data/data/bitclust/template/layout +1 -0
- data/data/bitclust/template.offline/function-index +1 -0
- data/data/bitclust/template.offline/layout +3 -0
- data/lib/bitclust/classentry.rb +1 -1
- data/lib/bitclust/docentry.rb +1 -1
- data/lib/bitclust/functionentry.rb +1 -1
- data/lib/bitclust/irb.rb +68 -0
- data/lib/bitclust/markdown_exporter.rb +311 -0
- data/lib/bitclust/mdcompiler.rb +15 -1
- data/lib/bitclust/mdparser.rb +42 -1
- data/lib/bitclust/methoddatabase.rb +4 -0
- data/lib/bitclust/methodentry.rb +24 -0
- data/lib/bitclust/methodsignature.rb +1 -1
- data/lib/bitclust/preprocessor.rb +4 -1
- data/lib/bitclust/rbs_overload_matcher.rb +179 -0
- data/lib/bitclust/rbs_sig_importer.rb +276 -0
- data/lib/bitclust/rbs_signatures.rb +125 -0
- data/lib/bitclust/rdcompiler.rb +12 -1
- data/lib/bitclust/runner.rb +2 -0
- data/lib/bitclust/screen.rb +4 -0
- data/lib/bitclust/search_index_generator.rb +28 -10
- data/lib/bitclust/searcher.rb +12 -2
- data/lib/bitclust/subcommands/rbssig_command.rb +99 -0
- data/lib/bitclust/subcommands/setup_command.rb +4 -1
- data/lib/bitclust/subcommands/statichtml_command.rb +59 -0
- data/lib/bitclust/version.rb +1 -1
- data/lib/bitclust-irb.rb +2 -0
- data/test/test_irb_plugin.rb +118 -0
- data/test/test_link_checker.rb +1 -1
- data/test/test_markdown_exporter.rb +260 -0
- data/test/test_mdcompiler.rb +47 -5
- data/test/test_mdparser.rb +219 -4
- data/test/test_methodentry.rb +111 -0
- data/test/test_methodsignature.rb +3 -1
- data/test/test_preprocessor.rb +26 -0
- data/test/test_rbs_overload_matcher.rb +170 -0
- data/test/test_rbs_sig_importer.rb +237 -0
- data/test/test_rbssig_command.rb +186 -0
- data/test/test_rdcompiler.rb +99 -1
- data/test/test_search_index_generator.rb +81 -0
- data/test/test_setup_command.rb +21 -0
- data/test/test_statichtml_command.rb +137 -0
- data/theme/default/js/run.js +14 -0
- data/theme/default/js/search_init.js +3 -1
- data/theme/default/js/search_page.js +7 -1
- data/theme/default/js/version_switcher.js +147 -0
- data/theme/default/search.css +21 -0
- data/theme/default/style.css +83 -7
- data/theme/lillia/style.css +26 -0
- metadata +35 -1
data/test/test_methodentry.rb
CHANGED
|
@@ -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)
|
data/test/test_preprocessor.rb
CHANGED
|
@@ -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
|