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
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test/unit'
4
+ require 'stringio'
5
+ require 'bitclust'
6
+ require 'bitclust/markdown_exporter'
7
+ require 'bitclust/mdparser'
8
+ require 'bitclust/methoddatabase'
9
+ require 'bitclust/functiondatabase'
10
+ require 'bitclust/subcommands/statichtml_command'
11
+
12
+ # MarkdownExporter: statichtml --markdown-output 用に、HTML の各ページと
13
+ # 対になる Markdown を前処理済みソースから組み立てる。
14
+ class TestMarkdownExporter < Test::Unit::TestCase
15
+ PARAMS = { "version" => "3.4" }
16
+
17
+ def setup
18
+ @urlmapper = BitClust::Subcommands::StatichtmlCommand::URLMapperEx.new(
19
+ :suffix => '.html',
20
+ :fs_casesensitive => true
21
+ )
22
+ @urlmapper.bitclust_html_base = '..'
23
+ @exporter = BitClust::MarkdownExporter.new(@urlmapper, '.html')
24
+ end
25
+
26
+ def parse_md(md, libname = "_builtin")
27
+ db = BitClust::MethodDatabase.dummy(PARAMS)
28
+ lib = BitClust::MDParser.new(db).parse(StringIO.new(md), libname, PARAMS)
29
+ [db, lib]
30
+ end
31
+
32
+ sub_test_case("convert_text") do
33
+ def test_bare_method_ref
34
+ assert_equal "[Array#index](../method/Array/i/index.md) を参照",
35
+ @exporter.convert_text("[m:Array#index] を参照")
36
+ end
37
+
38
+ def test_bare_class_ref
39
+ assert_equal "[String](../class/String.md)",
40
+ @exporter.convert_text("[c:String]")
41
+ end
42
+
43
+ def test_bare_library_ref
44
+ assert_equal "[csv](../library/csv.md)",
45
+ @exporter.convert_text("[lib:csv]")
46
+ end
47
+
48
+ def test_bare_doc_ref
49
+ assert_equal "[spec/literal](../doc/spec=2fliteral.md)",
50
+ @exporter.convert_text("[d:spec/literal]")
51
+ end
52
+
53
+ def test_bare_function_ref
54
+ assert_equal "[rb_ary_new](../function/rb_ary_new.md)",
55
+ @exporter.convert_text("[f:rb_ary_new]")
56
+ end
57
+
58
+ def test_module_function_ref
59
+ # md 表記の ?. は内部表記 .# に正規化して URL を引く(ラベルは書かれたまま)
60
+ assert_equal "[Kernel?.puts](../method/Kernel/m/puts.md)",
61
+ @exporter.convert_text("[m:Kernel?.puts]")
62
+ end
63
+
64
+ def test_method_ref_with_escaped_brackets
65
+ # [m:Array#\[\]] のようにエスケープされた参照。URL はエスケープを解いた
66
+ # メソッド名から引き、ラベルは書かれたまま(md として正しい形)を保つ
67
+ assert_equal "[Array#\\[\\]](../method/Array/i/=5b=5d.md)",
68
+ @exporter.convert_text("[m:Array#\\[\\]]")
69
+ end
70
+
71
+ def test_labeled_ref
72
+ assert_equal "[個数](../method/Array/i/size.md)",
73
+ @exporter.convert_text("[個数](m:Array#size)")
74
+ end
75
+
76
+ def test_labeled_ref_with_code_span_label
77
+ assert_equal "[`Array#size`](../method/Array/i/size.md)",
78
+ @exporter.convert_text("[`Array#size`](m:Array#size)")
79
+ end
80
+
81
+ def test_markdown_link_kept
82
+ assert_equal "[Ruby](https://www.ruby-lang.org/)",
83
+ @exporter.convert_text("[Ruby](https://www.ruby-lang.org/)")
84
+ end
85
+
86
+ def test_anchor_link_kept
87
+ assert_equal "[前述の例](#example)",
88
+ @exporter.convert_text("[前述の例](#example)")
89
+ end
90
+
91
+ def test_url_ref
92
+ assert_equal "[https://www.ruby-lang.org/](https://www.ruby-lang.org/)",
93
+ @exporter.convert_text("[url:https://www.ruby-lang.org/]")
94
+ end
95
+
96
+ def test_ref_in_page
97
+ assert_equal "[overview](#overview)",
98
+ @exporter.convert_text("[ref:overview]")
99
+ end
100
+
101
+ def test_ref_cross_page
102
+ assert_equal "[spec/literal#num](../doc/spec=2fliteral.md#num)",
103
+ @exporter.convert_text("[ref:d:spec/literal#num]")
104
+ end
105
+
106
+ def test_code_span_is_not_converted
107
+ assert_equal "`[m:Array#index]`",
108
+ @exporter.convert_text("`[m:Array#index]`")
109
+ end
110
+
111
+ def test_code_fence_is_not_converted
112
+ src = "```ruby\n# [m:Array#index]\n```\n[m:Array#index]\n"
113
+ expected = "```ruby\n# [m:Array#index]\n```\n[Array#index](../method/Array/i/index.md)\n"
114
+ assert_equal expected, @exporter.convert_text(src)
115
+ end
116
+
117
+ def test_unknown_ref_type_is_kept
118
+ assert_equal "[man:printf(3)]", @exporter.convert_text("[man:printf(3)]")
119
+ end
120
+
121
+ def test_unresolvable_method_spec_is_kept
122
+ # クラス名とメソッド名に分割できない指定はそのまま残す
123
+ assert_equal "[m:broken spec]", @exporter.convert_text("[m:broken spec]")
124
+ end
125
+
126
+ def test_escaped_bracket_is_kept
127
+ assert_equal "\\[m:Array#index]", @exporter.convert_text("\\[m:Array#index]")
128
+ end
129
+ end
130
+
131
+ sub_test_case("pages") do
132
+ CLASS_MD = <<~MD
133
+ ---
134
+ library: _builtin
135
+ ---
136
+ # class Array < Object
137
+
138
+ 配列クラス。[c:Enumerable] も参照。
139
+
140
+ ## Class Methods
141
+
142
+ ### def self.new(size) -> Array
143
+
144
+ 生成。
145
+
146
+ ## Instance Methods
147
+
148
+ ### def index(val) -> Integer
149
+ ### def find_index(val) -> Integer
150
+
151
+ 位置を返します。
152
+ MD
153
+
154
+ def test_class_page
155
+ _, lib = parse_md(CLASS_MD)
156
+ md = @exporter.class_page(lib.classes.first)
157
+ assert_equal <<~MD, md
158
+ # class Array < Object
159
+
160
+ 配列クラス。[Enumerable](../class/Enumerable.md) も参照。
161
+
162
+ ## Class Methods
163
+
164
+ - [new](../method/Array/s/new.md)
165
+
166
+ ## Instance Methods
167
+
168
+ - [find_index](../method/Array/i/find_index.md)
169
+ - [index](../method/Array/i/index.md)
170
+ MD
171
+ end
172
+
173
+ def test_module_page_heading
174
+ md = <<~MD
175
+ ---
176
+ library: _builtin
177
+ ---
178
+ # module Comparable
179
+
180
+ 比較。
181
+ MD
182
+ _, lib = parse_md(md)
183
+ page = @exporter.class_page(lib.classes.first)
184
+ assert page.start_with?("# module Comparable\n"), page
185
+ end
186
+
187
+ def test_method_page
188
+ _, lib = parse_md(CLASS_MD)
189
+ entry = lib.classes.first.entries.find { |e| e.names.include?("index") }
190
+ @urlmapper.bitclust_html_base = '../../..'
191
+ md = @exporter.method_page("Array#index", [entry])
192
+ assert_equal <<~MD, md
193
+ # Array#index
194
+
195
+ ### def index(val) -> Integer
196
+ ### def find_index(val) -> Integer
197
+
198
+ 位置を返します。
199
+ MD
200
+ end
201
+
202
+ def test_method_page_normalizes_module_function_name
203
+ _, lib = parse_md(CLASS_MD)
204
+ entry = lib.classes.first.entries.first
205
+ md = @exporter.method_page("Kernel.#puts", [entry])
206
+ assert md.start_with?("# Kernel?.puts\n"), md
207
+ end
208
+
209
+ def test_library_page
210
+ md = <<~MD
211
+ ---
212
+ type: library
213
+ category: FileFormat
214
+ ---
215
+ CSV を扱うライブラリです。
216
+ MD
217
+ _, lib = parse_md(md, "csv")
218
+ page = @exporter.library_page(lib)
219
+ assert_equal <<~MD, page
220
+ # library csv
221
+
222
+ CSV を扱うライブラリです。
223
+ MD
224
+ end
225
+
226
+ def test_doc_page
227
+ db = BitClust::MethodDatabase.dummy(PARAMS)
228
+ entry = BitClust::DocEntry.new(db, "help")
229
+ entry.title = "ヘルプ"
230
+ entry.source = "\n[c:String] を参照。\n"
231
+ page = @exporter.doc_page(entry)
232
+ assert_equal <<~MD, page
233
+ # ヘルプ
234
+
235
+ [String](../class/String.md) を参照。
236
+ MD
237
+ end
238
+
239
+ def test_function_page
240
+ require 'bitclust/functiondatabase'
241
+ md = <<~MD
242
+ ### VALUE rb_ary_new()
243
+
244
+ 空の配列を返します。
245
+ MD
246
+ db = BitClust::FunctionDatabase.dummy(PARAMS)
247
+ io = StringIO.new(md)
248
+ def io.path = "array.c.md"
249
+ BitClust::MDFunctionParser.new(db).parse(io, "array.c", PARAMS)
250
+ page = @exporter.function_page(db.functions.first)
251
+ assert_equal <<~MD, page
252
+ # rb_ary_new
253
+
254
+ ### VALUE rb_ary_new()
255
+
256
+ 空の配列を返します。
257
+ MD
258
+ end
259
+ end
260
+ end
@@ -23,7 +23,7 @@ class TestMDCompiler < Test::Unit::TestCase
23
23
  @rd = BitClust::RDCompiler.new(@u, 1, { :database => @db })
24
24
  end
25
25
 
26
- def compile_method(compiler, src)
26
+ def compile_method(compiler, src, rbs_signature_overloads: nil)
27
27
  method_entry = Object.new
28
28
  mock(method_entry).source { src }
29
29
  mock(method_entry).index_id.any_times { "dummy" }
@@ -32,6 +32,7 @@ class TestMDCompiler < Test::Unit::TestCase
32
32
  mock(method_entry).names.any_times { [] }
33
33
  mock(method_entry).since_map.any_times { {} }
34
34
  mock(method_entry).until_map.any_times { {} }
35
+ mock(method_entry).rbs_signature_overloads.any_times { rbs_signature_overloads }
35
36
  compiler.compile_method(method_entry)
36
37
  end
37
38
 
@@ -58,6 +59,47 @@ class TestMDCompiler < Test::Unit::TestCase
58
59
 
59
60
  # ---- メソッドエントリ ----
60
61
 
62
+ # RBS シグネチャ表示: MDCompiler も entry_chunk をオーバーライドしている
63
+ # ので、rd 経路と同じ位置(説明 <dd> の直前)に同じ <dt> が出ること
64
+ def test_rbs_signatures_dt_matches_rd_output
65
+ rd_src = "--- index(val) -> Integer\n\n説明\n"
66
+ overloads = [{'segments' => [['t', '() -> void']]}]
67
+ md_src = BitClust::RRDToMarkdown.convert(rd_src)
68
+ rd_html = compile_method(@rd, rd_src, rbs_signature_overloads: overloads)
69
+ md_html = compile_method(@md, md_src, rbs_signature_overloads: overloads)
70
+ assert_include md_html,
71
+ '<dt class="rbs-signature"><code>() &rarr; void</code></dt>'
72
+ assert_equal rd_html, md_html
73
+ end
74
+
75
+ # チャンク振り分けは md 側のシグネチャ行(### def ...)でも rd と同じに
76
+ # 動くこと(MDCompiler は行を "--- " に正規化してから照合する)
77
+ def test_rbs_signatures_chunk_assignment_matches_rd_output
78
+ rd_src = <<~RD
79
+ --- index(sub) -> Integer | nil
80
+
81
+ 文字列で探す。
82
+
83
+ --- index(pattern) {|s| ... } -> Integer | nil
84
+
85
+ ブロックで探す。
86
+ RD
87
+ overloads = [
88
+ {'segments' => [['t', '(String sub) -> Integer?']],
89
+ 'params' => ['sub'], 'arity' => [1, 1]},
90
+ {'segments' => [['t', '(Regexp pattern) { (String) -> bool } -> Integer?']],
91
+ 'params' => ['pattern'], 'arity' => [1, 1], 'block' => 'req'},
92
+ ]
93
+ md_src = BitClust::RRDToMarkdown.convert(rd_src)
94
+ rd_html = compile_method(@rd, rd_src, rbs_signature_overloads: overloads)
95
+ md_html = compile_method(@md, md_src, rbs_signature_overloads: overloads)
96
+ assert_match(
97
+ %r!<code>index\(pattern\).*?</dt>\n<dt class="rbs-signature"><code>\(Regexp pattern\) \{ \(String\) &rarr; bool \} &rarr; Integer\?</code></dt>\n<dd!m,
98
+ md_html)
99
+ assert_equal 2, md_html.scan('rbs-signature').size
100
+ assert_equal rd_html, md_html
101
+ end
102
+
61
103
  def test_method_signature_and_paragraph
62
104
  assert_equivalent_method <<~RD
63
105
  --- index(val) -> Integer
@@ -752,10 +794,10 @@ class TestMDCompiler < Test::Unit::TestCase
752
794
  def test_gfm_table_alignment
753
795
  md = "# T\n\n| a | b | c |\n|:--|:-:|--:|\n| 1 | 2 | 3 |\n"
754
796
  html = gfm_compiler.compile(md)
755
- assert_include html, '<th align="left">a</th>'
756
- assert_include html, '<th align="center">b</th>'
757
- assert_include html, '<th align="right">c</th>'
758
- assert_include html, '<td align="right">3</td>'
797
+ assert_include html, '<th style="text-align:left">a</th>'
798
+ assert_include html, '<th style="text-align:center">b</th>'
799
+ assert_include html, '<th style="text-align:right">c</th>'
800
+ assert_include html, '<td style="text-align:right">3</td>'
759
801
  end
760
802
 
761
803
  def test_gfm_pipe_line_without_delimiter_is_paragraph
@@ -82,7 +82,7 @@ class TestMDParser < Test::Unit::TestCase
82
82
 
83
83
  ## Class Methods
84
84
 
85
- ### def new -> Comparable
85
+ ### def Comparable.new -> Comparable
86
86
 
87
87
  生成。
88
88
  MD
@@ -181,7 +181,7 @@ class TestMDParser < Test::Unit::TestCase
181
181
  end
182
182
 
183
183
  def test_unsupported_signature_forms_are_rejected
184
- # RBS 形式(MARKUP_SPEC §3.2 で構想)と self. プレフィクスは描画側
184
+ # RBS 形式(MARKUP_SPEC §3.2 で構想)は描画側
185
185
  # (MethodSignature.parse)が受理できないため、パース時に拒否する
186
186
  base = <<~MD
187
187
  ---
@@ -197,12 +197,83 @@ class TestMDParser < Test::Unit::TestCase
197
197
 
198
198
  説明。
199
199
  MD
200
- ['### def each: () { (untyped) -> void } -> self',
201
- '### def self.new(size = 0) -> Foo'].each do |sig|
200
+ ['### def each: () { (untyped) -> void } -> self'].each do |sig|
202
201
  assert_raise(BitClust::ParseError, sig) { parse_md(base % sig) }
203
202
  end
204
203
  end
205
204
 
205
+ def test_self_and_class_name_prefixed_signatures
206
+ # クラスメソッドは `def self.name` / `def Klass.name` 形式でも書ける。
207
+ # どちらも singleton method として登録され、名前にプレフィクスは含まない
208
+ md = <<~MD
209
+ ---
210
+ library: _builtin
211
+ ---
212
+ # class Foo < Object
213
+
214
+ テスト。
215
+
216
+ ## Class Methods
217
+
218
+ ### def self.build(x) -> Foo
219
+
220
+ self. 形式。
221
+
222
+ ### def Foo.create(x) -> Foo
223
+
224
+ Klass. 形式。
225
+ MD
226
+ _, lib = parse_md(md)
227
+ entries = lib.classes.first.entries
228
+ assert_equal [["build"], ["create"]], entries.map(&:names)
229
+ assert_equal [:singleton_method, :singleton_method], entries.map(&:type)
230
+ # source には書かれたままの md が入る
231
+ assert_include entries[0].source, "### def self.build(x) -> Foo"
232
+ end
233
+
234
+ def test_main_prefixed_signature_on_object_page
235
+ # object main のページでは main. プレフィクスで特異メソッドを書ける
236
+ # (main は fatal・ARGF.class と同様に文法上クラス名として扱われる)
237
+ md = <<~MD
238
+ ---
239
+ library: _builtin
240
+ ---
241
+ # object main
242
+
243
+ テスト。
244
+
245
+ ## Class Methods
246
+
247
+ ### def main.include(*modules) -> self
248
+
249
+ main. 形式。
250
+ MD
251
+ _, lib = parse_md(md)
252
+ entries = lib.classes.first.entries
253
+ assert_equal [["include"]], entries.map(&:names)
254
+ assert_equal [:singleton_method], entries.map(&:type)
255
+ end
256
+
257
+ def test_self_prefixed_signature_in_instance_methods_section_is_rejected
258
+ # self. は singleton 確定なので Instance Methods セクションとは矛盾する
259
+ md = <<~MD
260
+ ---
261
+ library: _builtin
262
+ ---
263
+ # class Foo < Object
264
+
265
+ テスト。
266
+
267
+ ## Instance Methods
268
+
269
+ ### def self.bar -> nil
270
+
271
+ 矛盾。
272
+ MD
273
+ error = assert_raise(BitClust::ParseError) { parse_md(md) }
274
+ assert_include error.message, "signature crash"
275
+ end
276
+
206
277
  def test_library_file_with_category
207
278
  md = <<~MD
208
279
  ---
@@ -570,4 +641,113 @@ class TestMDParser < Test::Unit::TestCase
570
641
  assert_equal('2.0.0', entry.since_of('fuga'))
571
642
  assert_equal('3.0', entry.since_of('fuga2'))
572
643
  end
644
+
645
+ def test_entry_style_bare_def_in_singleton_section_is_rejected
646
+ # MARKUP_SPEC §3.1: 特異メソッドはプレフィクス必須(def 行だけで
647
+ # メソッド種別が分かるようにする)
648
+ md = <<~MD
649
+ ---
650
+ library: _builtin
651
+ ---
652
+ # class Foo < Object
653
+
654
+ テスト。
655
+
656
+ ## Class Methods
657
+
658
+ ### def build(x) -> Foo
659
+
660
+ プレフィクスなし。
661
+ MD
662
+ error = assert_raise(BitClust::ParseError) { parse_md(md) }
663
+ assert_include error.message, "singleton method entry must be written as `def Foo.name' or `def self.name'"
664
+ end
665
+
666
+ def test_entry_style_keyword_section_mismatch_is_rejected
667
+ # rurema/doctree#3291 型の分類間違い: キーワードとセクションの不一致
668
+ base = <<~MD
669
+ ---
670
+ library: _builtin
671
+ ---
672
+ # class Foo < Object
673
+
674
+ テスト。
675
+
676
+ ## %s
677
+
678
+ ### %s
679
+
680
+ 不一致。
681
+ MD
682
+ [
683
+ # Class Methods 配下の const(#3291 の実例)
684
+ ['Class Methods', 'const CURRENT -> Foo', "entry keyword `const' does not match this section (expected `def')"],
685
+ # Constants 配下の gvar
686
+ ['Constants', 'gvar $foo -> Foo', "entry keyword `gvar' does not match this section (expected `const')"],
687
+ # Instance Methods 配下の const
688
+ ['Instance Methods', 'const BAR -> Integer', "entry keyword `const' does not match this section (expected `def')"],
689
+ ].each do |section, sig, message|
690
+ error = assert_raise(BitClust::ParseError, "#{section} / #{sig}") do
691
+ parse_md(base % [section, sig])
692
+ end
693
+ assert_include error.message, message
694
+ end
695
+ end
696
+
697
+ def test_entry_style_module_functions_require_keyword
698
+ md = <<~MD
699
+ ---
700
+ library: _builtin
701
+ ---
702
+ # module Foo
703
+
704
+ テスト。
705
+
706
+ ## Module Functions
707
+
708
+ ### def bar -> nil
709
+
710
+ module_function キーワードなし。
711
+ MD
712
+ error = assert_raise(BitClust::ParseError) { parse_md(md) }
713
+ assert_include error.message, "entry keyword `def' does not match this section (expected `module_function def')"
714
+ end
715
+
716
+ def test_entry_style_accepts_consistent_forms
717
+ md = <<~MD
718
+ ---
719
+ library: _builtin
720
+ ---
721
+ # class Foo < Object
722
+
723
+ テスト。
724
+
725
+ ## Class Methods
726
+
727
+ ### def Foo.build(x) -> Foo
728
+
729
+ Klass 形式。
730
+
731
+ ### def self.create(x) -> Foo
732
+
733
+ self 形式。
734
+
735
+ ## Instance Methods
736
+
737
+ ### def each {|x| ... } -> self
738
+
739
+ 素の def。
740
+
741
+ ## Constants
742
+
743
+ ### const BAR -> Integer
744
+
745
+ 定数。
746
+ MD
747
+ _, lib = parse_md(md)
748
+ entries = lib.classes.first.entries
749
+ assert_equal [["build"], ["create"], ["each"], ["BAR"]], entries.map(&:names)
750
+ assert_equal [:singleton_method, :singleton_method, :instance_method, :constant],
751
+ entries.map(&:type)
752
+ end
573
753
  end
@@ -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)