bitclust-core 1.3.0 → 1.6.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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -0
  3. data/README.md +65 -0
  4. data/Rakefile +32 -0
  5. data/data/bitclust/catalog/ja_JP.UTF-8 +19 -1
  6. data/data/bitclust/searchpage/index.html +90 -0
  7. data/data/bitclust/template/class +69 -8
  8. data/data/bitclust/template/function +2 -2
  9. data/data/bitclust/template/layout +11 -3
  10. data/data/bitclust/template/method +2 -2
  11. data/data/bitclust/template/search +1 -1
  12. data/data/bitclust/template.epub/class +22 -1
  13. data/data/bitclust/template.epub/function +2 -2
  14. data/data/bitclust/template.epub/layout +1 -3
  15. data/data/bitclust/template.epub/method +2 -2
  16. data/data/bitclust/template.lillia/class +26 -5
  17. data/data/bitclust/template.lillia/layout +1 -3
  18. data/data/bitclust/template.lillia/method +2 -2
  19. data/data/bitclust/template.offline/class +48 -4
  20. data/data/bitclust/template.offline/function +2 -2
  21. data/data/bitclust/template.offline/layout +27 -3
  22. data/data/bitclust/template.offline/method +2 -2
  23. data/lib/bitclust/app.rb +16 -17
  24. data/lib/bitclust/capi_converter.rb +20 -0
  25. data/lib/bitclust/classentry.rb +67 -17
  26. data/lib/bitclust/compat.rb +2 -2
  27. data/lib/bitclust/completion.rb +58 -27
  28. data/lib/bitclust/crossrubyutils.rb +8 -5
  29. data/lib/bitclust/database.rb +28 -8
  30. data/lib/bitclust/doc_converter.rb +63 -0
  31. data/lib/bitclust/docentry.rb +1 -1
  32. data/lib/bitclust/entity_splitter.rb +221 -0
  33. data/lib/bitclust/entry.rb +47 -7
  34. data/lib/bitclust/functiondatabase.rb +13 -1
  35. data/lib/bitclust/functionentry.rb +5 -1
  36. data/lib/bitclust/functionreferenceparser.rb +4 -4
  37. data/lib/bitclust/generators/epub.rb +10 -3
  38. data/lib/bitclust/htmlutils.rb +1 -1
  39. data/lib/bitclust/include_graph.rb +375 -0
  40. data/lib/bitclust/include_pruner.rb +144 -0
  41. data/lib/bitclust/interface.rb +1 -1
  42. data/lib/bitclust/libraryentry.rb +8 -7
  43. data/lib/bitclust/lineinput.rb +7 -5
  44. data/lib/bitclust/link_checker.rb +179 -0
  45. data/lib/bitclust/markdown_bridge.rb +186 -0
  46. data/lib/bitclust/markdown_orchestrator.rb +267 -0
  47. data/lib/bitclust/markdown_to_rrd.rb +824 -0
  48. data/lib/bitclust/markdown_tree.rb +217 -0
  49. data/lib/bitclust/mdcompiler.rb +883 -0
  50. data/lib/bitclust/mdparser.rb +347 -0
  51. data/lib/bitclust/messagecatalog.rb +3 -3
  52. data/lib/bitclust/method_since_calculator.rb +117 -0
  53. data/lib/bitclust/methoddatabase.rb +124 -14
  54. data/lib/bitclust/methodentry.rb +125 -7
  55. data/lib/bitclust/methodid.rb +12 -11
  56. data/lib/bitclust/methodsignature.rb +10 -4
  57. data/lib/bitclust/nameutils.rb +41 -22
  58. data/lib/bitclust/preprocessor.rb +66 -29
  59. data/lib/bitclust/rdcompiler.rb +195 -95
  60. data/lib/bitclust/refsdatabase.rb +23 -8
  61. data/lib/bitclust/reloadable_request_handler.rb +72 -0
  62. data/lib/bitclust/requesthandler.rb +21 -16
  63. data/lib/bitclust/ridatabase.rb +10 -4
  64. data/lib/bitclust/rrd_to_markdown.rb +925 -0
  65. data/lib/bitclust/rrdparser.rb +196 -30
  66. data/lib/bitclust/runner.rb +17 -2
  67. data/lib/bitclust/screen.rb +80 -11
  68. data/lib/bitclust/search_index_generator.rb +288 -0
  69. data/lib/bitclust/searcher.rb +69 -20
  70. data/lib/bitclust/simplesearcher.rb +24 -12
  71. data/lib/bitclust/subcommand.rb +7 -1
  72. data/lib/bitclust/subcommands/ancestors_command.rb +9 -2
  73. data/lib/bitclust/subcommands/checklink_command.rb +54 -0
  74. data/lib/bitclust/subcommands/chm_command.rb +15 -7
  75. data/lib/bitclust/subcommands/classes_command.rb +5 -0
  76. data/lib/bitclust/subcommands/extract_command.rb +5 -0
  77. data/lib/bitclust/subcommands/htmlfile_command.rb +21 -7
  78. data/lib/bitclust/subcommands/init_command.rb +4 -1
  79. data/lib/bitclust/subcommands/list_command.rb +12 -4
  80. data/lib/bitclust/subcommands/lookup_command.rb +28 -9
  81. data/lib/bitclust/subcommands/methods_command.rb +10 -2
  82. data/lib/bitclust/subcommands/methodsince_command.rb +80 -0
  83. data/lib/bitclust/subcommands/preproc_command.rb +5 -0
  84. data/lib/bitclust/subcommands/property_command.rb +2 -1
  85. data/lib/bitclust/subcommands/searchpage_command.rb +102 -0
  86. data/lib/bitclust/subcommands/server_command.rb +10 -8
  87. data/lib/bitclust/subcommands/setup_command.rb +24 -10
  88. data/lib/bitclust/subcommands/statichtml_command.rb +129 -6
  89. data/lib/bitclust/subcommands/update_command.rb +110 -2
  90. data/lib/bitclust/syntax_highlighter.rb +48 -4
  91. data/lib/bitclust/textutils.rb +3 -3
  92. data/lib/bitclust/version.rb +1 -1
  93. data/lib/bitclust/version_badges.rb +90 -0
  94. data/lib/bitclust/whole_file_gate.rb +137 -0
  95. data/test/test_app.rb +269 -0
  96. data/test/test_bitclust.rb +2 -0
  97. data/test/test_capi_converter.rb +91 -0
  98. data/test/test_class_screen.rb +413 -0
  99. data/test/test_copy_doc_md.rb +53 -0
  100. data/test/test_doc_converter.rb +78 -0
  101. data/test/test_entity_splitter.rb +231 -0
  102. data/test/test_entry.rb +207 -2
  103. data/test/test_eol_warning.rb +43 -0
  104. data/test/test_functionreferenceparser.rb +11 -0
  105. data/test/test_include_graph.rb +640 -0
  106. data/test/test_include_pruner.rb +133 -0
  107. data/test/test_link_checker.rb +114 -0
  108. data/test/test_lookup_command.rb +135 -0
  109. data/test/test_markdown_bridge.rb +217 -0
  110. data/test/test_markdown_orchestrator.rb +478 -0
  111. data/test/test_markdown_to_rrd.rb +757 -0
  112. data/test/test_markdown_tree.rb +369 -0
  113. data/test/test_mdcompiler.rb +1276 -0
  114. data/test/test_mdparser.rb +538 -0
  115. data/test/test_method_screen.rb +166 -0
  116. data/test/test_method_since_calculator.rb +290 -0
  117. data/test/test_methoddatabase.rb +16 -0
  118. data/test/test_methodentry.rb +328 -0
  119. data/test/test_methodsignature.rb +3 -1
  120. data/test/test_methodsince_command.rb +168 -0
  121. data/test/test_nameutils.rb +22 -0
  122. data/test/test_preprocessor.rb +215 -0
  123. data/test/test_rdcompiler.rb +401 -30
  124. data/test/test_refsdatabase.rb +32 -0
  125. data/test/test_rrd_to_markdown.rb +968 -0
  126. data/test/test_rrdparser.rb +295 -2
  127. data/test/test_run_ruby_wasm.rb +54 -0
  128. data/test/test_run_worker_prelude.rb +83 -0
  129. data/test/test_runner.rb +51 -0
  130. data/test/test_search_index_generator.rb +513 -0
  131. data/test/test_search_screen.rb +48 -0
  132. data/test/test_searcher.rb +132 -0
  133. data/test/test_searchpage_command.rb +167 -0
  134. data/test/test_simplesearcher.rb +28 -0
  135. data/test/test_statichtml_command.rb +204 -0
  136. data/test/test_syntax_highlighter.rb +60 -0
  137. data/test/test_whole_file_gate.rb +175 -0
  138. data/theme/default/js/NOTICE +42 -0
  139. data/theme/default/js/run-worker.js +111 -0
  140. data/theme/default/js/run.js +350 -0
  141. data/theme/default/js/search_controller.js +134 -0
  142. data/theme/default/js/search_init.js +176 -0
  143. data/theme/default/js/search_navigation.js +110 -0
  144. data/theme/default/js/search_page.js +202 -0
  145. data/theme/default/js/search_ranker.js +244 -0
  146. data/theme/default/script.js +91 -18
  147. data/theme/default/search.css +149 -0
  148. data/theme/default/style.css +193 -21
  149. data/theme/default/syntax-highlight.css +8 -0
  150. data/theme/lillia/style.css +45 -1
  151. metadata +161 -20
  152. data/README +0 -32
@@ -0,0 +1,640 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test/unit'
4
+ require 'tmpdir'
5
+ require 'fileutils'
6
+
7
+ require 'bitclust/include_graph'
8
+
9
+ # IncludeGraph: doctree の #@include グラフを版ゲート付きで解析する faithful 層。
10
+ # スコープ([3.0,4.2) 等)は適用せず、生の全所属・全ゲートを収集する。
11
+ # スコープ適用は IncludeGraph::Scope(別テストセクション)が担う。
12
+ #
13
+ # テストリスト:
14
+ # [x] LIBRARIES の 1行1ライブラリ → root (<name>.rd) から grouping メンバーを収集
15
+ # [x] 分類: 最初の非空・非#@行が = class|module|object|reopen|redefine → grouping、他 → fragment
16
+ # [x] パス解決: include 元ディレクトリ相対、target そのまま → 無ければ target.rd
17
+ # [x] LIBRARIES の重複エントリは dedupe
18
+ # [x] include サイトの #@since/#@until → membership conditions
19
+ # [x] #@else で最内条件を反転(since↔until、同一バージョン)
20
+ # [x] #@if は版制約なしの条件として記録(#@else でも版制約なしのまま)
21
+ # [x] ネストした条件は全て積む、#@end で pop
22
+ # [x] LIBRARIES 内の版ゲートはそのライブラリの全 membership の先頭条件になる
23
+ # [x] 再帰: grouping メンバーが include する grouping にも経路の条件を継承した membership
24
+ # [x] 多重所属: 複数経路から include される grouping は memberships 複数(生のまま保持)
25
+ # [x] 同一 (library, conditions) の重複 membership は dedupe
26
+ # [x] 循環 include で無限ループしない(warning 記録)
27
+ # [x] 存在しない include 対象 → warning 記録(エラーにしない)
28
+ # [x] interval: conditions → [lo, hi)(since は max、until は min、:if は無視)
29
+ # [x] Scope#cover?: 範囲 [lo, hi) と交差するか
30
+ # [x] Scope#gate: front matter に書く since/until(スコープ境界内のみ、範囲外は省略)
31
+ class TestIncludeGraph < Test::Unit::TestCase
32
+ def analyze(files)
33
+ Dir.mktmpdir do |dir|
34
+ files.each do |path, content|
35
+ full = File.join(dir, path)
36
+ FileUtils.mkdir_p(File.dirname(full))
37
+ File.write(full, content)
38
+ end
39
+ return BitClust::IncludeGraph.analyze(dir)
40
+ end
41
+ end
42
+
43
+ def membership(library, conditions = [])
44
+ BitClust::IncludeGraph::Membership.new(
45
+ library,
46
+ conditions.map { |kind, version| BitClust::IncludeGraph::Condition.new(kind, version) }
47
+ )
48
+ end
49
+
50
+ # ---- roots と分類 ----
51
+
52
+ def test_simple_grouping_membership
53
+ graph = analyze(
54
+ "LIBRARIES" => "foo\n",
55
+ "foo.rd" => "\#@include(foo/Bar)\n",
56
+ "foo/Bar" => "= class Bar < Object\n"
57
+ )
58
+ assert_equal [membership("foo")], graph.memberships("foo/Bar")
59
+ end
60
+
61
+ def test_fragment_classification
62
+ graph = analyze(
63
+ "LIBRARIES" => "foo\n",
64
+ "foo.rd" => "\#@include(foo/Bar)\n\#@include(foo/frag)\n",
65
+ "foo/Bar" => "= class Bar < Object\n",
66
+ "foo/frag" => "ただの散文断片。\n"
67
+ )
68
+ assert_equal ["foo/Bar"], graph.groupings.keys
69
+ assert_equal ["foo/frag"], graph.fragments
70
+ end
71
+
72
+ def test_classification_skips_blank_and_directive_lines
73
+ graph = analyze(
74
+ "LIBRARIES" => "foo\n",
75
+ "foo.rd" => "\#@include(foo/Bar)\n",
76
+ "foo/Bar" => "\n\#@since 1.9.1\n= reopen Kernel\n\#@end\n"
77
+ )
78
+ assert_equal ["foo/Bar"], graph.groupings.keys
79
+ end
80
+
81
+ def test_include_resolves_with_rd_fallback
82
+ graph = analyze(
83
+ "LIBRARIES" => "foo\n",
84
+ "foo.rd" => "\#@include(foo/Bar)\n",
85
+ "foo/Bar.rd" => "= class Bar < Object\n"
86
+ )
87
+ assert_equal [membership("foo")], graph.memberships("foo/Bar.rd")
88
+ end
89
+
90
+ def test_include_resolves_relative_to_including_file
91
+ graph = analyze(
92
+ "LIBRARIES" => "foo/sub\n",
93
+ "foo/sub.rd" => "\#@include(Bar)\n",
94
+ "foo/Bar" => "= class Bar < Object\n"
95
+ )
96
+ assert_equal [membership("foo/sub")], graph.memberships("foo/Bar")
97
+ end
98
+
99
+ def test_libraries_skips_preprocessor_comment_lines
100
+ # LIBRARIES には「#@# json/add/rails.rd」のようなコメントアウト行がある
101
+ graph = analyze(
102
+ "LIBRARIES" => "foo\n\#@# ghost\n",
103
+ "foo.rd" => "\#@include(foo/Bar)\n",
104
+ "foo/Bar" => "= class Bar < Object\n"
105
+ )
106
+ assert_equal [], graph.warnings
107
+ assert_equal [membership("foo")], graph.memberships("foo/Bar")
108
+ end
109
+
110
+ def test_duplicate_libraries_entries_are_deduped
111
+ graph = analyze(
112
+ "LIBRARIES" => "foo\nfoo\n",
113
+ "foo.rd" => "\#@include(foo/Bar)\n",
114
+ "foo/Bar" => "= class Bar < Object\n"
115
+ )
116
+ assert_equal [membership("foo")], graph.memberships("foo/Bar")
117
+ end
118
+
119
+ # ---- 版ゲート ----
120
+
121
+ def test_include_under_since_gate
122
+ graph = analyze(
123
+ "LIBRARIES" => "foo\n",
124
+ "foo.rd" => "\#@since 3.2\n\#@include(foo/Bar)\n\#@end\n",
125
+ "foo/Bar" => "= class Bar < Object\n"
126
+ )
127
+ assert_equal [membership("foo", [[:since, "3.2"]])], graph.memberships("foo/Bar")
128
+ end
129
+
130
+ def test_include_in_else_branch_inverts_since
131
+ # thread.rd パターン: #@since 2.3.0 ... #@else [include群] #@end → 実効 until 2.3.0
132
+ graph = analyze(
133
+ "LIBRARIES" => "foo\n",
134
+ "foo.rd" => "\#@since 2.3.0\ntext\n\#@else\n\#@include(foo/Bar)\n\#@end\n",
135
+ "foo/Bar" => "= class Bar < Object\n"
136
+ )
137
+ assert_equal [membership("foo", [[:until, "2.3.0"]])], graph.memberships("foo/Bar")
138
+ end
139
+
140
+ def test_include_in_else_branch_inverts_until
141
+ graph = analyze(
142
+ "LIBRARIES" => "foo\n",
143
+ "foo.rd" => "\#@until 1.9.1\ntext\n\#@else\n\#@include(foo/Bar)\n\#@end\n",
144
+ "foo/Bar" => "= class Bar < Object\n"
145
+ )
146
+ assert_equal [membership("foo", [[:since, "1.9.1"]])], graph.memberships("foo/Bar")
147
+ end
148
+
149
+ def test_include_under_if_gate_records_condition_without_version_bound
150
+ graph = analyze(
151
+ "LIBRARIES" => "foo\n",
152
+ "foo.rd" => "\#@if (version < \"1.8.2\")\n\#@include(foo/Bar)\n\#@end\n",
153
+ "foo/Bar" => "= class Bar < Object\n"
154
+ )
155
+ assert_equal [membership("foo", [[:if, '(version < "1.8.2")']])],
156
+ graph.memberships("foo/Bar")
157
+ end
158
+
159
+ def test_include_in_else_branch_of_if_records_negation
160
+ graph = analyze(
161
+ "LIBRARIES" => "foo\n",
162
+ "foo.rd" => "\#@if (version < \"1.8.2\")\ntext\n\#@else\n\#@include(foo/Bar)\n\#@end\n",
163
+ "foo/Bar" => "= class Bar < Object\n"
164
+ )
165
+ assert_equal [membership("foo", [[:if, '!((version < "1.8.2"))']])],
166
+ graph.memberships("foo/Bar")
167
+ end
168
+
169
+ def test_nested_gates_accumulate_and_end_pops
170
+ src = <<~RRD
171
+ \#@since 2.0.0
172
+ \#@until 3.0
173
+ \#@include(foo/Bar)
174
+ \#@end
175
+ \#@include(foo/Baz)
176
+ \#@end
177
+ \#@include(foo/Qux)
178
+ RRD
179
+ graph = analyze(
180
+ "LIBRARIES" => "foo\n",
181
+ "foo.rd" => src,
182
+ "foo/Bar" => "= class Bar < Object\n",
183
+ "foo/Baz" => "= class Baz < Object\n",
184
+ "foo/Qux" => "= class Qux < Object\n"
185
+ )
186
+ assert_equal [membership("foo", [[:since, "2.0.0"], [:until, "3.0"]])],
187
+ graph.memberships("foo/Bar")
188
+ assert_equal [membership("foo", [[:since, "2.0.0"]])], graph.memberships("foo/Baz")
189
+ assert_equal [membership("foo")], graph.memberships("foo/Qux")
190
+ end
191
+
192
+ def test_libraries_gate_prefixes_membership_conditions
193
+ # LIBRARIES の cmath は #@until 2.7.0 に包まれている実例に対応
194
+ graph = analyze(
195
+ "LIBRARIES" => "\#@until 2.7.0\nfoo\n\#@end\nother\n",
196
+ "foo.rd" => "\#@since 1.9.1\n\#@include(foo/Bar)\n\#@end\n",
197
+ "other.rd" => "text\n",
198
+ "foo/Bar" => "= class Bar < Object\n"
199
+ )
200
+ assert_equal [membership("foo", [[:until, "2.7.0"], [:since, "1.9.1"]])],
201
+ graph.memberships("foo/Bar")
202
+ end
203
+
204
+ # ---- 再帰と多重所属 ----
205
+
206
+ def test_nested_grouping_inherits_path_conditions
207
+ # rdoc/context.rd → RDoc__Context のような2段 grouping
208
+ graph = analyze(
209
+ "LIBRARIES" => "foo\n",
210
+ "foo.rd" => "\#@since 2.0.0\n\#@include(foo/Bar)\n\#@end\n",
211
+ "foo/Bar" => "= class Bar < Object\n\#@until 3.0\n\#@include(Baz)\n\#@end\n",
212
+ "foo/Baz" => "= class Baz < Object\n"
213
+ )
214
+ assert_equal [membership("foo", [[:since, "2.0.0"], [:until, "3.0"]])],
215
+ graph.memberships("foo/Baz")
216
+ end
217
+
218
+ def test_fragment_include_from_member
219
+ # _builtin/Array → pack-template のような断片 include
220
+ graph = analyze(
221
+ "LIBRARIES" => "foo\n",
222
+ "foo.rd" => "\#@include(foo/Bar)\n",
223
+ "foo/Bar" => "= class Bar < Object\n\#@include(frag)\n",
224
+ "foo/frag" => "断片テキスト。\n"
225
+ )
226
+ assert_equal ["foo/frag"], graph.fragments
227
+ assert_equal [], graph.memberships("foo/frag")
228
+ end
229
+
230
+ def test_multiple_memberships_are_kept_raw
231
+ # cgi/core.rd が util.rd(cgi/util のルート)を #@until 内で include する実例に対応
232
+ graph = analyze(
233
+ "LIBRARIES" => "foo\nbar\n",
234
+ "foo.rd" => "\#@include(shared/Thing)\n",
235
+ "bar.rd" => "\#@until 1.9.1\n\#@include(shared/Thing)\n\#@end\n",
236
+ "shared/Thing" => "= class Thing < Object\n"
237
+ )
238
+ assert_equal [
239
+ membership("foo"),
240
+ membership("bar", [[:until, "1.9.1"]]),
241
+ ], graph.memberships("shared/Thing")
242
+ end
243
+
244
+ def test_identical_memberships_are_deduped
245
+ # _builtin/Data → Data.attention 9回のような同一 include の繰り返し(grouping 版)
246
+ graph = analyze(
247
+ "LIBRARIES" => "foo\n",
248
+ "foo.rd" => "\#@include(foo/Bar)\n\#@include(foo/Bar)\n",
249
+ "foo/Bar" => "= class Bar < Object\n"
250
+ )
251
+ assert_equal [membership("foo")], graph.memberships("foo/Bar")
252
+ end
253
+
254
+ def test_entity_h1_without_space_is_grouping
255
+ # RRDParser は /\A=[^=]/ で H1 を認識するため「=class Encoding」も有効
256
+ # (実データ: _builtin/Encoding, _builtin/Encoding__Converter)
257
+ graph = analyze(
258
+ "LIBRARIES" => "foo\n",
259
+ "foo.rd" => "\#@include(foo/Bar)\n",
260
+ "foo/Bar" => "=class Bar < Object\n"
261
+ )
262
+ assert_equal [membership("foo")], graph.memberships("foo/Bar")
263
+ end
264
+
265
+ def test_membership_propagates_through_fragment_chain
266
+ # fiddle.rd → fiddle/2.0/fiddle.lib.rd(散文開始=fragment)→ Fiddle(grouping)の
267
+ # transclusion チェーン。fragment を経由しても所属と経路条件は伝播する
268
+ graph = analyze(
269
+ "LIBRARIES" => "foo\n",
270
+ "foo.rd" => "\#@until 2.0.0\n\#@include(foo/1.9/frag.rd)\n\#@else\n\#@include(foo/2.0/frag.rd)\n\#@end\n",
271
+ "foo/1.9/frag.rd" => "散文。\n\#@include(Bar)\n",
272
+ "foo/2.0/frag.rd" => "散文。\n\#@include(Bar)\n",
273
+ "foo/1.9/Bar" => "= class Bar < Object\n",
274
+ "foo/2.0/Bar" => "= class Bar < Object\n"
275
+ )
276
+ assert_equal [membership("foo", [[:until, "2.0.0"]])], graph.memberships("foo/1.9/Bar")
277
+ assert_equal [membership("foo", [[:since, "2.0.0"]])], graph.memberships("foo/2.0/Bar")
278
+ assert_equal ["foo/1.9/frag.rd", "foo/2.0/frag.rd"], graph.fragments
279
+ end
280
+
281
+ def test_include_with_dotdot_path_is_normalized
282
+ # rdoc/parsers/parse_c.rd の #@include(../RDoc__KNOWN_CLASSES) に対応。
283
+ # 正規化しないと同一ファイルが別キーで二重登録される
284
+ graph = analyze(
285
+ "LIBRARIES" => "foo\n",
286
+ "foo.rd" => "\#@include(foo/sub/Baz)\n\#@include(foo/Bar)\n",
287
+ "foo/sub/Baz" => "= class Baz < Object\n\#@include(../Bar)\n",
288
+ "foo/Bar" => "= class Bar < Object\n"
289
+ )
290
+ assert_equal ["foo/Bar", "foo/sub/Baz"], graph.groupings.keys
291
+ assert_equal [membership("foo")], graph.memberships("foo/Bar")
292
+ end
293
+
294
+ def test_include_cycle_terminates_with_warning
295
+ graph = analyze(
296
+ "LIBRARIES" => "foo\n",
297
+ "foo.rd" => "\#@include(foo/Bar)\n",
298
+ "foo/Bar" => "= class Bar < Object\n\#@include(Baz)\n",
299
+ "foo/Baz" => "= class Baz < Object\n\#@include(Bar)\n"
300
+ )
301
+ assert graph.warnings.any? { |w| w.include?("cycle") },
302
+ "expected cycle warning, got: #{graph.warnings.inspect}"
303
+ end
304
+
305
+ def test_missing_include_target_records_warning
306
+ graph = analyze(
307
+ "LIBRARIES" => "foo\n",
308
+ "foo.rd" => "\#@include(foo/Nothing)\n"
309
+ )
310
+ assert_equal ["include target not found: foo/Nothing (from foo.rd)"], graph.warnings
311
+ end
312
+
313
+ def test_missing_library_root_records_warning
314
+ graph = analyze("LIBRARIES" => "ghost\n")
315
+ assert_equal ["library root not found: ghost.rd"], graph.warnings
316
+ end
317
+
318
+ def test_samplecode_block_end_does_not_pop_version_gate
319
+ # #@samplecode ブロックも #@end で閉じるため、pop の対応を取る必要がある
320
+ src = <<~RRD
321
+ \#@since 3.2
322
+ \#@samplecode 例
323
+ p 1
324
+ \#@end
325
+ \#@include(foo/Bar)
326
+ \#@end
327
+ \#@include(foo/Baz)
328
+ RRD
329
+ graph = analyze(
330
+ "LIBRARIES" => "foo\n",
331
+ "foo.rd" => src,
332
+ "foo/Bar" => "= class Bar < Object\n",
333
+ "foo/Baz" => "= class Baz < Object\n"
334
+ )
335
+ assert_equal [membership("foo", [[:since, "3.2"]])], graph.memberships("foo/Bar")
336
+ assert_equal [membership("foo")], graph.memberships("foo/Baz")
337
+ end
338
+
339
+ def test_include_allows_space_before_paren
340
+ # Preprocessor は #@include\s*\( を許容する
341
+ graph = analyze(
342
+ "LIBRARIES" => "foo\n",
343
+ "foo.rd" => "\#@include (foo/Bar)\n",
344
+ "foo/Bar" => "= class Bar < Object\n"
345
+ )
346
+ assert_equal [membership("foo")], graph.memberships("foo/Bar")
347
+ end
348
+
349
+ # ---- interval とスコープ適用 ----
350
+
351
+ def conditions(*pairs)
352
+ pairs.map { |kind, version| BitClust::IncludeGraph::Condition.new(kind, version) }
353
+ end
354
+
355
+ def interval(*pairs)
356
+ BitClust::IncludeGraph.interval(conditions(*pairs))
357
+ end
358
+
359
+ def test_interval_unconstrained
360
+ assert_equal [nil, nil], interval
361
+ end
362
+
363
+ def test_interval_since_takes_max_and_until_takes_min
364
+ assert_equal [Gem::Version.new("2.3.0"), nil], interval([:since, "2.3.0"])
365
+ assert_equal [nil, Gem::Version.new("2.7.0")], interval([:until, "2.7.0"])
366
+ assert_equal [Gem::Version.new("2.3.0"), Gem::Version.new("3.0")],
367
+ interval([:since, "1.9.1"], [:since, "2.3.0"], [:until, "3.1"], [:until, "3.0"])
368
+ end
369
+
370
+ def test_interval_ignores_if_conditions
371
+ assert_equal [Gem::Version.new("3.2"), nil],
372
+ interval([:if, '(version > "1.8.2")'], [:since, "3.2"])
373
+ end
374
+
375
+ def test_scope_cover
376
+ scope = BitClust::IncludeGraph::Scope.new("3.0", "4.2")
377
+ assert_true scope.cover?(conditions) # 無条件
378
+ assert_true scope.cover?(conditions([:since, "3.2"])) # Data
379
+ assert_true scope.cover?(conditions([:since, "2.3.0"])) # 下限跨ぎ
380
+ assert_true scope.cover?(conditions([:until, "3.1"])) # 上限側交差
381
+ assert_false scope.cover?(conditions([:until, "2.3.0"])) # thread 旧所属
382
+ assert_false scope.cover?(conditions([:until, "3.0"])) # 境界: [_,3.0) は交差なし
383
+ assert_false scope.cover?(conditions([:since, "4.2"])) # 境界: [4.2,_) は交差なし
384
+ assert_false scope.cover?(conditions([:since, "3.5"], [:until, "3.2"])) # 空区間
385
+ assert_true scope.cover?(conditions([:if, 'x'])) # :if は制約なし扱い
386
+ end
387
+
388
+ def test_scope_gate_keeps_only_bounds_inside_scope
389
+ scope = BitClust::IncludeGraph::Scope.new("3.0", "4.2")
390
+ assert_equal({}, scope.gate(conditions))
391
+ assert_equal({ since: "3.2" }, scope.gate(conditions([:since, "3.2"])))
392
+ assert_equal({}, scope.gate(conditions([:since, "2.3.0"]))) # 下限以下は省略
393
+ assert_equal({}, scope.gate(conditions([:since, "3.0"]))) # 下限ちょうども省略
394
+ assert_equal({ until: "4.0" }, scope.gate(conditions([:until, "4.0"])))
395
+ assert_equal({ since: "3.2", until: "4.0" },
396
+ scope.gate(conditions([:since, "3.2"], [:until, "4.0"])))
397
+ assert_nil scope.gate(conditions([:until, "2.3.0"])) # スコープ外
398
+ end
399
+
400
+ def test_scope_gate_preserves_original_version_spelling
401
+ scope = BitClust::IncludeGraph::Scope.new("3.0", "4.2")
402
+ assert_equal({ since: "3.2.0" }, scope.gate(conditions([:since, "3.2.0"])))
403
+ end
404
+
405
+ def test_scope_always_and_never
406
+ scope = BitClust::IncludeGraph::Scope.new("3.0", "4.2")
407
+ cond = ->(kind, v) { BitClust::IncludeGraph::Condition.new(kind, v) }
408
+ # since v: スコープ全体で真 ⇔ v <= 下限
409
+ assert_true scope.always?(cond[:since, "2.3.0"])
410
+ assert_true scope.always?(cond[:since, "3.0"])
411
+ assert_false scope.always?(cond[:since, "3.2"])
412
+ assert_false scope.never?(cond[:since, "3.2"])
413
+ assert_true scope.never?(cond[:since, "4.2"])
414
+ # until v: スコープ全体で真 ⇔ v >= 上限
415
+ assert_true scope.always?(cond[:until, "4.2"])
416
+ assert_false scope.always?(cond[:until, "3.1"])
417
+ assert_true scope.never?(cond[:until, "3.0"])
418
+ assert_false scope.never?(cond[:until, "3.1"])
419
+ # if は原則判定不能(常に false)
420
+ assert_false scope.always?(cond[:if, "(version > \"1.8\")"])
421
+ assert_false scope.never?(cond[:if, "(version > \"1.8\")"])
422
+ end
423
+
424
+ def test_scope_never_for_provable_if_conditions
425
+ # LIBRARIES の ubygems: #@if("1.9.1" <= version and version < "2.5.0")
426
+ scope = BitClust::IncludeGraph::Scope.new("3.0", "4.2")
427
+ cond = ->(v) { BitClust::IncludeGraph::Condition.new(:if, v) }
428
+ assert_true scope.never?(cond['("1.9.1" <= version and version < "2.5.0")'])
429
+ assert_true scope.never?(cond['(version < "1.8.2")'])
430
+ assert_true scope.never?(cond['(version == "2.0.0")'])
431
+ assert_false scope.never?(cond['(version >= "1.8.2")'])
432
+ assert_false scope.never?(cond['(version == "3.1")'])
433
+ assert_false scope.never?(cond['(version < "4.0")']) # スコープ内で真偽が変わる
434
+ end
435
+
436
+ def test_scope_cover_excludes_never_if_conditions
437
+ scope = BitClust::IncludeGraph::Scope.new("3.0", "4.2")
438
+ conds = [BitClust::IncludeGraph::Condition.new(:if, '("1.9.1" <= version and version < "2.5.0")')]
439
+ assert_false scope.cover?(conds)
440
+ assert_nil scope.gate(conds)
441
+ end
442
+
443
+ def test_scope_gate_decomposes_conjunctive_if_into_bounds
444
+ # 旧版サルベージ: ubygems の #@if("1.9.1" <= version and version < "2.5.0") は
445
+ # ワイドスコープでは in-scope になり、since/until に分解して front matter へ
446
+ scope = BitClust::IncludeGraph::Scope.new("1.8.7", "4.2")
447
+ conds = [BitClust::IncludeGraph::Condition.new(:if, '("1.9.1" <= version and version < "2.5.0")')]
448
+ assert_true scope.cover?(conds)
449
+ assert_equal({ since: "1.9.1", until: "2.5.0" }, scope.gate(conds))
450
+ end
451
+
452
+ def test_scope_gate_decomposes_simple_if_forms
453
+ scope = BitClust::IncludeGraph::Scope.new("1.8.7", "4.2")
454
+ cond = ->(v) { [BitClust::IncludeGraph::Condition.new(:if, v)] }
455
+ # version >= "X" 形(rss)。X がスコープ下限以下なら無条件
456
+ assert_equal({}, scope.gate(cond['(version >= "1.8.2")']))
457
+ assert_equal({ since: "2.0.0" }, scope.gate(cond['(version >= "2.0.0")']))
458
+ # version < "X" 形は until X
459
+ assert_equal({ until: "2.0.0" }, scope.gate(cond['(version < "2.0.0")']))
460
+ # 分解できない形(<= / 否定)は境界に寄与しない(従来どおり無条件扱い)
461
+ assert_equal({}, scope.gate(cond['(version <= "2.0.0")']))
462
+ assert_equal({}, scope.gate(cond['!((version < "2.0.0"))']))
463
+ end
464
+
465
+ # ---- front_matter_map: メンバーへの注入値(スコープ適用済み)----
466
+
467
+ def scope30_42
468
+ BitClust::IncludeGraph::Scope.new("3.0", "4.2")
469
+ end
470
+
471
+ def test_front_matter_map_simple_membership
472
+ graph = analyze(
473
+ "LIBRARIES" => "foo\n",
474
+ "foo.rd" => "\#@include(foo/Bar)\n",
475
+ "foo/Bar" => "= class Bar < Object\n"
476
+ )
477
+ assert_equal({ "foo/Bar" => { "library" => "foo" } }, graph.front_matter_map(scope30_42))
478
+ end
479
+
480
+ def test_front_matter_map_with_structural_gate
481
+ graph = analyze(
482
+ "LIBRARIES" => "foo\n",
483
+ "foo.rd" => "\#@since 3.2\n\#@include(foo/Bar)\n\#@end\n",
484
+ "foo/Bar" => "= class Bar < Object\n"
485
+ )
486
+ assert_equal({ "foo/Bar" => { "library" => "foo", "since" => "3.2" } },
487
+ graph.front_matter_map(scope30_42))
488
+ end
489
+
490
+ def test_front_matter_map_excludes_out_of_scope_members
491
+ graph = analyze(
492
+ "LIBRARIES" => "foo\n",
493
+ "foo.rd" => "\#@until 2.4.0\n\#@include(foo/Bar)\n\#@end\n",
494
+ "foo/Bar" => "= class Bar < Object\n"
495
+ )
496
+ assert_equal({}, graph.front_matter_map(scope30_42))
497
+ end
498
+
499
+ def test_front_matter_map_multi_library_membership_becomes_list
500
+ # 同時多重所属(旧 rdoc の code_objects+個別ファイル等)は
501
+ # ゲート付きリスト形式で表現する(旧挙動の警告スキップを置換)
502
+ graph = analyze(
503
+ "LIBRARIES" => "foo\nbar\n",
504
+ "foo.rd" => "\#@include(shared/Thing)\n",
505
+ "bar.rd" => "\#@include(shared/Thing)\n",
506
+ "shared/Thing" => "= class Thing < Object\n"
507
+ )
508
+ assert_equal(
509
+ { "shared/Thing" => { "library" => [
510
+ { "name" => "bar" }, { "name" => "foo" }
511
+ ] } },
512
+ graph.front_matter_map(scope30_42))
513
+ assert graph.warnings.none? { |w| w.include?("multi-membership") },
514
+ "multi-membership should be supported now, got: #{graph.warnings.inspect}"
515
+ end
516
+
517
+ def test_front_matter_map_multi_membership_orders_modern_side_first
518
+ # Mutex 型: 片方は無条件、片方は until 付き →
519
+ # 「現在まで存在する側」(until なし)を先頭に置く
520
+ graph = analyze(
521
+ "LIBRARIES" => "thread\nbuiltin\n",
522
+ "builtin.rd" => "\#@include(shared/Mutex)\n",
523
+ "thread.rd" => "\#@until 3.1\n\#@include(shared/Mutex)\n\#@end\n",
524
+ "shared/Mutex" => "= class Mutex < Object\n"
525
+ )
526
+ assert_equal(
527
+ { "shared/Mutex" => { "library" => [
528
+ { "name" => "builtin" },
529
+ { "name" => "thread", "until" => "3.1" }
530
+ ] } },
531
+ graph.front_matter_map(scope30_42))
532
+ end
533
+
534
+ def test_front_matter_map_multi_membership_partition_has_no_entity_gate
535
+ # ConditionVariable 型: since X / until X の相補分割。
536
+ # エンティティ自体はスコープ全域に存在するので since/until キーは付かない
537
+ graph = analyze(
538
+ "LIBRARIES" => "thread\nbuiltin\n",
539
+ "builtin.rd" => "\#@since 3.2\n\#@include(shared/CV)\n\#@end\n",
540
+ "thread.rd" => "\#@until 3.2\n\#@include(shared/CV)\n\#@end\n",
541
+ "shared/CV" => "= class ConditionVariable < Object\n"
542
+ )
543
+ fm = graph.front_matter_map(scope30_42)
544
+ assert_equal(
545
+ [{ "name" => "builtin", "since" => "3.2" },
546
+ { "name" => "thread", "until" => "3.2" }],
547
+ fm["shared/CV"]["library"])
548
+ assert_nil fm["shared/CV"]["since"]
549
+ assert_nil fm["shared/CV"]["until"]
550
+ end
551
+
552
+ def test_front_matter_map_takes_interval_hull_within_same_library
553
+ # 同一ライブラリから複数サイトで include される場合、エンティティは
554
+ # いずれかのサイトが有効なら存在する → ゲートは区間の hull(弱い方)を取る
555
+ graph = analyze(
556
+ "LIBRARIES" => "foo\n",
557
+ "foo.rd" => "\#@since 3.2\n\#@include(foo/Bar)\n\#@end\n" \
558
+ "\#@since 3.1\n\#@include(foo/Bar)\n\#@end\n",
559
+ "foo/Bar" => "= class Bar < Object\n"
560
+ )
561
+ assert_equal({ "foo/Bar" => { "library" => "foo", "since" => "3.1" } },
562
+ graph.front_matter_map(scope30_42))
563
+ end
564
+
565
+ def test_front_matter_map_unconditional_site_wins_over_gated_site
566
+ graph = analyze(
567
+ "LIBRARIES" => "foo\n",
568
+ "foo.rd" => "\#@include(foo/Bar)\n\#@since 3.2\n\#@include(foo/Bar)\n\#@end\n",
569
+ "foo/Bar" => "= class Bar < Object\n"
570
+ )
571
+ assert_equal({ "foo/Bar" => { "library" => "foo" } }, graph.front_matter_map(scope30_42))
572
+ end
573
+
574
+ # ---- grouping_include_sites: prune 対象(ファイル => 記載どおりの target)----
575
+
576
+ def test_grouping_include_sites
577
+ graph = analyze(
578
+ "LIBRARIES" => "foo\n",
579
+ "foo.rd" => "\#@include(foo/Bar)\n\#@include(foo/frag)\n",
580
+ "foo/Bar" => "= class Bar < Object\n\#@include(Baz)\n\#@include(frag2)\n",
581
+ "foo/Baz" => "= class Baz < Object\n",
582
+ "foo/frag" => "断片。\n",
583
+ "foo/frag2" => "断片2。\n"
584
+ )
585
+ # fragment include(foo/frag, frag2)は含まれない。target は記載どおりの文字列
586
+ assert_equal({ "foo.rd" => ["foo/Bar"], "foo/Bar" => ["Baz"] },
587
+ graph.grouping_include_sites)
588
+ end
589
+
590
+ def test_grouping_include_sites_dedupes_multi_path_walks
591
+ # 同一ファイルが複数経路で走査されてもサイトは重複しない
592
+ graph = analyze(
593
+ "LIBRARIES" => "foo\nbar\n",
594
+ "foo.rd" => "\#@include(shared/frag)\n",
595
+ "bar.rd" => "\#@include(shared/frag)\n",
596
+ "shared/frag" => "断片。\n\#@include(Thing)\n",
597
+ "shared/Thing" => "= class Thing < Object\n"
598
+ )
599
+ assert_equal({ "shared/frag" => ["Thing"] }, graph.grouping_include_sites)
600
+ end
601
+
602
+ # ---- library_front_matter_map: ライブラリ概要ファイルへの注入値 ----
603
+
604
+ def test_library_front_matter_map_marks_roots
605
+ graph = analyze(
606
+ "LIBRARIES" => "foo\nbar/sub\n",
607
+ "foo.rd" => "本文。\n",
608
+ "bar/sub.rd" => "本文。\n"
609
+ )
610
+ assert_equal(
611
+ { "foo.rd" => { "type" => "library" }, "bar/sub.rd" => { "type" => "library" } },
612
+ graph.library_front_matter_map(scope30_42)
613
+ )
614
+ end
615
+
616
+ def test_library_front_matter_map_applies_scoped_gate
617
+ # LIBRARIES の fiber: until 3.1 / set: until 3.2 に相当
618
+ graph = analyze(
619
+ "LIBRARIES" => "\#@until 3.1\nfoo\n\#@end\nbar\n",
620
+ "foo.rd" => "本文。\n",
621
+ "bar.rd" => "本文。\n"
622
+ )
623
+ assert_equal(
624
+ { "foo.rd" => { "type" => "library", "until" => "3.1" },
625
+ "bar.rd" => { "type" => "library" } },
626
+ graph.library_front_matter_map(scope30_42)
627
+ )
628
+ end
629
+
630
+ def test_library_front_matter_map_excludes_out_of_scope_libraries
631
+ # cmath/scanf/sync(until 2.7.0)は [3.0,4.2) の対象外
632
+ graph = analyze(
633
+ "LIBRARIES" => "\#@until 2.7.0\nfoo\n\#@end\nbar\n",
634
+ "foo.rd" => "本文。\n",
635
+ "bar.rd" => "本文。\n"
636
+ )
637
+ assert_equal({ "bar.rd" => { "type" => "library" } },
638
+ graph.library_front_matter_map(scope30_42))
639
+ end
640
+ end