bitclust-core 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -0
  3. data/data/bitclust/catalog/ja_JP.UTF-8 +4 -2
  4. data/data/bitclust/template/function-index +1 -0
  5. data/data/bitclust/template/layout +1 -0
  6. data/data/bitclust/template.offline/function-index +1 -0
  7. data/data/bitclust/template.offline/layout +3 -0
  8. data/lib/bitclust/classentry.rb +1 -1
  9. data/lib/bitclust/docentry.rb +1 -1
  10. data/lib/bitclust/functionentry.rb +1 -1
  11. data/lib/bitclust/irb.rb +68 -0
  12. data/lib/bitclust/markdown_exporter.rb +311 -0
  13. data/lib/bitclust/mdcompiler.rb +15 -1
  14. data/lib/bitclust/mdparser.rb +42 -1
  15. data/lib/bitclust/methoddatabase.rb +4 -0
  16. data/lib/bitclust/methodentry.rb +24 -0
  17. data/lib/bitclust/methodsignature.rb +1 -1
  18. data/lib/bitclust/preprocessor.rb +4 -1
  19. data/lib/bitclust/rbs_overload_matcher.rb +179 -0
  20. data/lib/bitclust/rbs_sig_importer.rb +276 -0
  21. data/lib/bitclust/rbs_signatures.rb +125 -0
  22. data/lib/bitclust/rdcompiler.rb +12 -1
  23. data/lib/bitclust/runner.rb +2 -0
  24. data/lib/bitclust/screen.rb +4 -0
  25. data/lib/bitclust/search_index_generator.rb +28 -10
  26. data/lib/bitclust/searcher.rb +12 -2
  27. data/lib/bitclust/subcommands/rbssig_command.rb +99 -0
  28. data/lib/bitclust/subcommands/setup_command.rb +4 -1
  29. data/lib/bitclust/subcommands/statichtml_command.rb +59 -0
  30. data/lib/bitclust/version.rb +1 -1
  31. data/lib/bitclust-irb.rb +2 -0
  32. data/test/test_irb_plugin.rb +118 -0
  33. data/test/test_link_checker.rb +1 -1
  34. data/test/test_markdown_exporter.rb +260 -0
  35. data/test/test_mdcompiler.rb +47 -5
  36. data/test/test_mdparser.rb +219 -4
  37. data/test/test_methodentry.rb +111 -0
  38. data/test/test_methodsignature.rb +3 -1
  39. data/test/test_preprocessor.rb +26 -0
  40. data/test/test_rbs_overload_matcher.rb +170 -0
  41. data/test/test_rbs_sig_importer.rb +237 -0
  42. data/test/test_rbssig_command.rb +186 -0
  43. data/test/test_rdcompiler.rb +99 -1
  44. data/test/test_search_index_generator.rb +81 -0
  45. data/test/test_setup_command.rb +21 -0
  46. data/test/test_statichtml_command.rb +137 -0
  47. data/theme/default/js/run.js +14 -0
  48. data/theme/default/js/search_init.js +3 -1
  49. data/theme/default/js/search_page.js +7 -1
  50. data/theme/default/js/version_switcher.js +147 -0
  51. data/theme/default/search.css +21 -0
  52. data/theme/default/style.css +83 -7
  53. data/theme/lillia/style.css +26 -0
  54. metadata +35 -1
@@ -159,6 +159,7 @@ module BitClust
159
159
  # 版範囲の省略記法(#285): #%version A...B は半開区間 [A, B)。
160
160
  # Ruby の終端排他 Range リテラルに合わせて3点ドット(.. は終端を含むと
161
161
  # 誤解しやすいため受け付けない)。A... は A 以上、...B は B 未満。
162
+ # ドットなしの単一版 V はその版のみ(version == "V")。
162
163
  # 版はダブルクォート付きでもよい
163
164
  VERSION_LITERAL = /"(\d+(?:\.\d+)*)"|(\d+(?:\.\d+)*)/
164
165
  def build_cond_by_range(line)
@@ -170,8 +171,10 @@ module BitClust
170
171
  %Q(version >= "#{$1 || $2}")
171
172
  when /\A\.\.\.#{VERSION_LITERAL}\z/o
172
173
  %Q(version < "#{$1 || $2}")
174
+ when /\A#{VERSION_LITERAL}\z/o
175
+ %Q(version == "#{$1 || $2}")
173
176
  else
174
- parse_error "wrong version range (expected A...B / A... / ...B)", line
177
+ parse_error "wrong version range (expected V / A...B / A... / ...B)", line
175
178
  end
176
179
  end
177
180
 
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # bitclust/rbs_overload_matcher.rb
4
+ #
5
+ # RBS のオーバーロードを、引数パターンごとに分かれた説明チャンク
6
+ # (Array.new / Array#[] のように同名メソッドの `---` 見出し+説明の組が
7
+ # 複数あるもの)へ振り分ける。RBS の引数名は rdoc の call-seq 由来で
8
+ # rurema のシグネチャの引数名とよく一致するため、
9
+ # 「ブロック有無 → 引数名の一致 → アリティ範囲の重なり」のスコアで
10
+ # 最も近いチャンクを選ぶ。1:1 対応は原理的に保証されない
11
+ # (RBS の () と (int size, ?Elem val) が rurema では同一チャンク等)ので、
12
+ # 多対 1 の割り当てとし、確実な対応が取れないオーバーロードは先頭チャンクに
13
+ # 寄せる(どのチャンクにも重複しては出さない)。
14
+ #
15
+ # オーバーロードのメタ情報(params/arity/block)は RbsSigImporter が
16
+ # rbs_sig property の JSON に書き込んだものをそのまま(文字列キーの Hash で)
17
+ # 受け取る。チャンク側は正規化済みシグネチャ行("--- " 始まり)を
18
+ # MethodSignature.parse で解析する。
19
+
20
+ require 'bitclust/methodsignature'
21
+ require 'bitclust/exception'
22
+
23
+ module BitClust
24
+
25
+ module RbsOverloadMatcher
26
+
27
+ module_function
28
+
29
+ # overloads: rbs_sig JSON の overloads 配列(文字列キー Hash の配列)
30
+ # chunks: チャンクごとの正規化済みシグネチャ行("--- " 始まり)の配列
31
+ # 返り値: チャンクごとのオーバーロード添字の配列(chunks と同じ長さ)
32
+ def assign(overloads, chunks)
33
+ result = Array.new(chunks.size) { [] } #: Array[Array[Integer]]
34
+ return result if chunks.empty?
35
+ if chunks.size == 1
36
+ result[0] = (0...overloads.size).to_a
37
+ return result
38
+ end
39
+ features = chunks.map {|lines| chunk_features(lines) }
40
+ overloads.each_index do |i|
41
+ ov = overload_features(overloads[i])
42
+ result[ov ? best_chunk(ov, features) : 0] << i
43
+ end
44
+ result
45
+ end
46
+
47
+ # チャンクのシグネチャ行群から照合用の特徴を合算する。
48
+ # 1 行も解析できなければ nil(振り分け先にしない)
49
+ def chunk_features(sig_lines)
50
+ names = [] #: Array[String]
51
+ min = nil #: Integer?
52
+ max = 0 #: Integer?
53
+ block = false
54
+ parsed = false
55
+ sig_lines.each do |line|
56
+ begin
57
+ sig = MethodSignature.parse(line)
58
+ rescue ParseError
59
+ next
60
+ end
61
+ parsed = true
62
+ block ||= !sig.block.nil?
63
+ pmin, pmax, pnames, pblock = params_features(sig.params)
64
+ block ||= pblock
65
+ names.concat(pnames)
66
+ min = pmin if min.nil? || pmin < min
67
+ max = (max && pmax) ? [max, pmax].max : nil
68
+ end
69
+ return nil unless parsed
70
+ { names: names, min: min || 0, max: max, block: block }
71
+ end
72
+
73
+ # rbs_sig JSON のオーバーロード 1 個からスコア用の特徴を取り出す。
74
+ # メタ情報が無いもの(#322 以前の旧形式・attr・(?) 型)は nil
75
+ def overload_features(overload)
76
+ arity = overload['arity']
77
+ params = overload['params']
78
+ block = overload['block']
79
+ return nil unless arity || params || block
80
+ { names: params || [],
81
+ min: arity && arity[0],
82
+ max: arity && arity[1],
83
+ block: block }
84
+ end
85
+
86
+ # ---- 以下は実装詳細(module_function なので呼べてしまうが非公開扱い) ----
87
+
88
+ def best_chunk(overload, features)
89
+ best = 0
90
+ best_score = nil #: Integer?
91
+ features.each_with_index do |chunk, i|
92
+ next unless chunk
93
+ s = score(overload, chunk)
94
+ if best_score.nil? || s > best_score
95
+ best = i
96
+ best_score = s
97
+ end
98
+ end
99
+ best
100
+ end
101
+
102
+ # ブロックの一致(または矛盾)を最優先に、引数名の一致 > アリティ範囲の
103
+ # 重なりで加点する。同点は先頭チャンク優先(best_chunk が > で更新)
104
+ def score(overload, chunk)
105
+ s = 0
106
+ case overload[:block]
107
+ when 'req'
108
+ s += chunk[:block] ? 2 : -3
109
+ when 'opt'
110
+ s += 1
111
+ else
112
+ s += chunk[:block] ? -3 : 2
113
+ end
114
+ s += 2 * (overload[:names] & chunk[:names]).size
115
+ if overload[:min] && chunk[:min]
116
+ omax = overload[:max] || Float::INFINITY
117
+ cmax = chunk[:max] || Float::INFINITY
118
+ s += (overload[:min] <= cmax && chunk[:min] <= omax) ? 1 : -2
119
+ end
120
+ s
121
+ end
122
+
123
+ # シグネチャの引数リスト文字列 → [最小アリティ, 最大アリティ(nil=無制限),
124
+ # 引数名(位置引数+キーワード名。**kwrest 名も含む), ブロック有無(&引数)]
125
+ def params_features(params)
126
+ return [0, 0, [], false] if params.nil? || params.strip.empty?
127
+ required = 0
128
+ optional = 0
129
+ rest = false
130
+ block = false
131
+ names = [] #: Array[String]
132
+ split_top_level(params).each do |token|
133
+ token = token.strip
134
+ next if token.empty?
135
+ name = token[/[A-Za-z_]\w*/]
136
+ case token
137
+ when /\A&/
138
+ block = true
139
+ when /\A\*\*/
140
+ names << name if name
141
+ when /\A\*/, '...'
142
+ rest = true
143
+ names << name if name
144
+ when /\A[A-Za-z_]\w*:/
145
+ names << (name || raise)
146
+ when /=/
147
+ optional += 1
148
+ names << name if name
149
+ else
150
+ required += 1
151
+ names << name if name
152
+ end
153
+ end
154
+ [required, rest ? nil : required + optional, names, block]
155
+ end
156
+
157
+ # 括弧((), [], {})のネストを無視して最上位の ',' で分割する
158
+ # (デフォルト値の中の ',' で切らないため)
159
+ def split_top_level(str)
160
+ tokens = [+''] #: Array[String]
161
+ depth = 0
162
+ str.each_char do |c|
163
+ case c
164
+ when '(', '[', '{' then depth += 1
165
+ when ')', ']', '}' then depth -= 1
166
+ when ','
167
+ if depth == 0
168
+ tokens << +''
169
+ next
170
+ end
171
+ end
172
+ tokens.last << c
173
+ end
174
+ tokens
175
+ end
176
+
177
+ end
178
+
179
+ end
@@ -0,0 +1,276 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # bitclust/rbs_sig_importer.rb
4
+ #
5
+ # RBS 型シグネチャ(.rbs)を読み込み、"Class#method"/"Class.method" キー →
6
+ # オーバーロード配列の対応表を作って MethodEntry の rbs_sig property に
7
+ # 書き込む(rbssig サブコマンドが DB 構築後に呼ぶ)。
8
+ #
9
+ # オーバーロードは Hash 1 個: "segments" = [種別, テキスト] の組の配列
10
+ # (種別 "t" は素のテキスト、"c" はクラス/モジュール名で、描画側=
11
+ # RbsSignatures が DB に存在するものだけリンク化する。テキストを連結すると
12
+ # 元のシグネチャ文字列に戻る)に加え、説明チャンクへの振り分け
13
+ # (RbsOverloadMatcher)用のメタ情報 "params"(引数名)・"arity"
14
+ # ([最小, 最大|nil])・"block"("req"/"opt")を持つ。型名の位置は RBS
15
+ # パーサの location で厳密に取る。rbs gem が必要なのは書き込み側のこの
16
+ # ファイルだけで、描画側は property を読むだけで動く。
17
+
18
+ require 'rbs'
19
+ require 'json'
20
+ require 'pathname'
21
+
22
+ module BitClust
23
+
24
+ class RbsSigImporter
25
+
26
+ # core_root: ruby/rbs チェックアウトの core/(組み込みクラスの型)
27
+ # repository_root: 同 stdlib/(core_root を与えると rbs が stringio を
28
+ # 暗黙に要求するので、core_root とセットで渡す)
29
+ # sig_dirs: 素の .rbs ディレクトリ(テスト・追加分)
30
+ def initialize(core_root: nil, repository_root: nil, sig_dirs: [])
31
+ @core_root = core_root
32
+ @repository_root = repository_root
33
+ @sig_dirs = sig_dirs
34
+ @signatures = nil
35
+ end
36
+
37
+ # "Class#method"/"Class.method" → オーバーロード配列
38
+ def signatures
39
+ @signatures ||= build_signatures
40
+ end
41
+
42
+ # bitclust の typechar('i'/'s'/'m')と名前の並び(別名)で対応表を引く。
43
+ # モジュール関数(m)は def self?. 由来の # キーを先に、new は
44
+ # Class.new → Class#initialize の順で引く。見つからなければ nil
45
+ def lookup(class_name, typechar, names)
46
+ names.each do |name|
47
+ case typechar
48
+ when 'i'
49
+ sig = signatures["#{class_name}##{name}"]
50
+ when 's'
51
+ sig = signatures["#{class_name}.#{name}"]
52
+ sig ||= signatures["#{class_name}#initialize"] if name == 'new'
53
+ when 'm'
54
+ sig = signatures["#{class_name}##{name}"] || signatures["#{class_name}.#{name}"]
55
+ else
56
+ return nil
57
+ end
58
+ return sig if sig
59
+ end
60
+ nil
61
+ end
62
+
63
+ # DB の全メソッドエントリに対応表を引き、ヒットしたものへ rbs_sig
64
+ # property(JSON 1 行)を書き込む。書き方は MethodSinceCalculator#apply と
65
+ # 同じ(変更があったエントリだけ save)
66
+ def apply(db)
67
+ stats = { entries_updated: 0, sigs_matched: 0, methods_missed: 0 } #: stats
68
+ db.classes.each do |c|
69
+ c.entries.each do |m|
70
+ next if m.kind == :undefined
71
+ next unless %w[i s m].include?(m.typechar)
72
+ overloads = lookup(c.name, m.typechar, m.names)
73
+ unless overloads
74
+ stats[:methods_missed] += 1
75
+ next
76
+ end
77
+ stats[:sigs_matched] += 1
78
+ json = JSON.generate({'overloads' => overloads})
79
+ next if m.rbs_sig == json
80
+ m.rbs_sig = json
81
+ m.save
82
+ stats[:entries_updated] += 1
83
+ end
84
+ end
85
+ stats
86
+ end
87
+
88
+ private
89
+
90
+ def build_signatures
91
+ env = load_environment
92
+ sigs = {} #: signatures
93
+ aliases = [] #: Array[[String, untyped]]
94
+ env.class_decls.each do |type_name, entry|
95
+ class_name = type_name.to_s.delete_prefix('::')
96
+ each_decl(entry) do |decl|
97
+ decl.members.each do |member|
98
+ case member
99
+ when RBS::AST::Members::MethodDefinition
100
+ overloads = member.overloads.map {|o| method_type_overload(o.method_type.to_s) }
101
+ method_keys(class_name, member.name, member.kind).each do |key|
102
+ sigs[key] ||= overloads
103
+ end
104
+ when RBS::AST::Members::AttrReader
105
+ sigs[attr_key(class_name, member, member.name)] ||= [attr_overload(member.type.to_s)]
106
+ when RBS::AST::Members::AttrWriter
107
+ sigs[attr_key(class_name, member, "#{member.name}=")] ||= [attr_overload(member.type.to_s)]
108
+ when RBS::AST::Members::AttrAccessor
109
+ overloads = [attr_overload(member.type.to_s)]
110
+ sigs[attr_key(class_name, member, member.name)] ||= overloads
111
+ sigs[attr_key(class_name, member, "#{member.name}=")] ||= overloads
112
+ when RBS::AST::Members::Alias
113
+ aliases << [class_name, member]
114
+ end
115
+ end
116
+ end
117
+ end
118
+ resolve_aliases(sigs, aliases)
119
+ sigs
120
+ end
121
+
122
+ def load_environment
123
+ repository = RBS::Repository.new(no_stdlib: true)
124
+ if (repository_root = @repository_root)
125
+ repository.add(Pathname(repository_root))
126
+ end
127
+ core_root = @core_root
128
+ loader = RBS::EnvironmentLoader.new(
129
+ core_root: core_root && Pathname(core_root),
130
+ repository: repository)
131
+ @sig_dirs.each {|dir| loader.add(path: Pathname(dir)) }
132
+ env = RBS::Environment.new
133
+ loader.load(env: env)
134
+ env
135
+ end
136
+
137
+ # rbs 3.x は MultiEntry#decls、rbs 4.x は entry.each_decl
138
+ def each_decl(entry, &block)
139
+ if entry.respond_to?(:each_decl)
140
+ entry.each_decl(&block)
141
+ else
142
+ entry.decls.each {|d| block.call(d.decl) }
143
+ end
144
+ end
145
+
146
+ # def self?.(kind :singleton_instance)は Class.name と Class#name の
147
+ # 両方を定義する(bitclust のモジュール関数に対応)ので両キーに登録する
148
+ def method_keys(class_name, name, kind)
149
+ case kind
150
+ when :singleton
151
+ ["#{class_name}.#{name}"]
152
+ when :singleton_instance
153
+ ["#{class_name}.#{name}", "#{class_name}##{name}"]
154
+ else
155
+ ["#{class_name}##{name}"]
156
+ end
157
+ end
158
+
159
+ def attr_key(class_name, member, name)
160
+ member.kind == :singleton ? "#{class_name}.#{name}" : "#{class_name}##{name}"
161
+ end
162
+
163
+ # alias メンバーは元メソッドのシグネチャを共有する。alias が alias を
164
+ # 指すこともあるので、解決が進まなくなるまで繰り返す
165
+ def resolve_aliases(sigs, aliases)
166
+ until aliases.empty?
167
+ resolved, aliases = aliases.partition {|class_name, member|
168
+ sigs.key?(alias_key(class_name, member, member.old_name))
169
+ }
170
+ break if resolved.empty?
171
+ resolved.each do |class_name, member|
172
+ sigs[alias_key(class_name, member, member.new_name)] ||=
173
+ sigs[alias_key(class_name, member, member.old_name)]
174
+ end
175
+ end
176
+ end
177
+
178
+ def alias_key(class_name, member, name)
179
+ member.kind == :singleton ? "#{class_name}.#{name}" : "#{class_name}##{name}"
180
+ end
181
+
182
+ # メソッド型シグネチャ 1 行をオーバーロード(segments+メタ情報)にする
183
+ def method_type_overload(sig)
184
+ method_type = RBS::Parser.parse_method_type(sig, require_eof: true)
185
+ locs = [] #: Array[[String, Integer]]
186
+ collect_function_type_names(method_type.type, locs)
187
+ collect_function_type_names(method_type.block.type, locs) if method_type.block
188
+ overload = {'segments' => build_segments(sig, locs)} #: overload
189
+ add_overload_meta(overload, method_type)
190
+ overload
191
+ end
192
+
193
+ # attr 用: 型だけの文字列をオーバーロード(segments のみ)にする
194
+ def attr_overload(sig)
195
+ type = RBS::Parser.parse_type(sig, require_eof: true)
196
+ locs = [] #: Array[[String, Integer]]
197
+ collect_type_names(type, locs)
198
+ {'segments' => build_segments(sig, locs)}
199
+ end
200
+
201
+ # チャンク振り分け(RbsOverloadMatcher)用のメタ情報。引数名は位置引数
202
+ # (rest 含む)+キーワード名。アリティは位置引数の [最小, 最大] で
203
+ # rest があれば最大 nil(無制限)。`(?)`(UntypedFunction)には位置引数の
204
+ # 概念が無いので params/arity は付けない
205
+ def add_overload_meta(overload, method_type)
206
+ fun = method_type.type
207
+ if fun.respond_to?(:required_positionals)
208
+ names = [] #: Array[String]
209
+ positionals = fun.required_positionals + fun.optional_positionals +
210
+ fun.trailing_positionals
211
+ positionals.each {|p| names << p.name.to_s if p.name }
212
+ rest = fun.rest_positionals
213
+ names << rest.name.to_s if rest && rest.name
214
+ fun.required_keywords.each_key {|k| names << k.to_s }
215
+ fun.optional_keywords.each_key {|k| names << k.to_s }
216
+ required = fun.required_positionals.size + fun.trailing_positionals.size
217
+ overload['params'] = names
218
+ overload['arity'] =
219
+ [required, rest ? nil : required + fun.optional_positionals.size]
220
+ end
221
+ if (block = method_type.block)
222
+ overload['block'] = block.required ? 'req' : 'opt'
223
+ end
224
+ end
225
+
226
+ def collect_function_type_names(fun, locs)
227
+ fun.each_param {|param| collect_type_names(param.type, locs) }
228
+ collect_type_names(fun.return_type, locs)
229
+ end
230
+
231
+ # クラス/モジュール参照(ClassInstance)の名前とその開始位置を集める。
232
+ # location の :name 子は "::String" のように名前空間プレフィクスを含む
233
+ # ので、end_pos から正規化名の長さを引いて名前本体の開始位置にする
234
+ # (:: はテキストセグメント側に残る)
235
+ def collect_type_names(type, locs)
236
+ case type
237
+ when RBS::Types::ClassInstance
238
+ name = type.name.to_s.delete_prefix('::')
239
+ if (loc = type.location)
240
+ name_loc = loc[:name] || loc
241
+ locs << [name, name_loc.end_pos - name.length]
242
+ end
243
+ type.args.each {|t| collect_type_names(t, locs) }
244
+ when RBS::Types::Union, RBS::Types::Intersection, RBS::Types::Tuple
245
+ type.types.each {|t| collect_type_names(t, locs) }
246
+ when RBS::Types::Optional
247
+ collect_type_names(type.type, locs)
248
+ when RBS::Types::Record
249
+ type.all_fields.each_value {|t, _required| collect_type_names(t, locs) }
250
+ when RBS::Types::Alias
251
+ # エイリアス型(string, hash 等)自体はクラスではないのでテキストの
252
+ # ままにするが、ジェネリクス引数の中のクラス名は拾う
253
+ type.args.each {|t| collect_type_names(t, locs) }
254
+ when RBS::Types::Proc
255
+ collect_function_type_names(type.type, locs)
256
+ end
257
+ end
258
+
259
+ def build_segments(sig, locs)
260
+ segments = [] #: segment_line
261
+ pos = 0
262
+ locs.sort_by {|_name, start| start }.each do |name, start|
263
+ # 位置が既出セグメントと重なる・実文字列と一致しない型は
264
+ # リンク化を諦めてテキストのまま残す(取りこぼしても表示は壊れない)
265
+ next if start < pos || sig[start, name.length] != name
266
+ segments << ['t', sig[pos...start] || raise] if start > pos
267
+ segments << ['c', name]
268
+ pos = start + name.length
269
+ end
270
+ segments << ['t', sig[pos..] || raise] if pos < sig.length
271
+ segments
272
+ end
273
+
274
+ end
275
+
276
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # bitclust/rbs_signatures.rb
4
+ #
5
+ # メソッドエントリの RBS 型シグネチャ(rbs_sig property。RbsSigImporter が
6
+ # DB 構築後に書き込む)を、シグネチャ見出し(<dt> 群)の直後・説明 <dd> の
7
+ # 直前に 1 オーバーロード 1 行の <dt class="rbs-signature"> として描画する。
8
+ # dd だと字下げで説明の一部に見え、pre だと theme/script.js が COPY ボタンを
9
+ # 付けてしまうため、見出しと同じ dt+code の並びにする。property が無い
10
+ # エントリでは何も出さないので、出力は従来とバイト一致のまま。
11
+ #
12
+ # Array.new / Array#[] のように引数パターンごとに説明チャンクが分かれる
13
+ # メソッドでは、compile_method 冒頭でソースのシグネチャ行を走査して
14
+ # チャンク列を作り、RbsOverloadMatcher で各オーバーロードを最も近い
15
+ # チャンクへ振り分けておく(entry_chunk は自分の番のぶんだけ描画する)。
16
+ #
17
+ # RDCompiler(md は MDCompiler が継承)に include される。escape_html /
18
+ # class_link(いずれも HTMLUtils)と @option[:database] にだけ依存し、
19
+ # rbs gem は使わない(型名の位置は property 側で解決済み)。
20
+
21
+ require 'bitclust/rbs_overload_matcher'
22
+
23
+ module BitClust
24
+
25
+ module RbsSignatures
26
+
27
+ # 振り分け状態はメソッドのコンパイル専用(compile_method が
28
+ # prepare_rbs_signatures で作り直す)なので、他のコンパイルへ
29
+ # 持ち越さないよう setup(全コンパイル共通)からリセットする
30
+ def reset_rbs_signatures
31
+ @rbs_sig_by_chunk = nil
32
+ @rbs_chunk_index = 0
33
+ end
34
+
35
+ # compile_method 冒頭で呼ぶ。rbs_sig property があれば、source の
36
+ # シグネチャ行からチャンク列を作りオーバーロードを振り分けておく
37
+ def prepare_rbs_signatures(entry, source)
38
+ reset_rbs_signatures
39
+ overloads = entry.rbs_signature_overloads
40
+ return unless overloads
41
+ chunks = rbs_scan_signature_chunks(source)
42
+ if chunks.empty?
43
+ no_signatures = [] #: Array[String]
44
+ chunks = [no_signatures]
45
+ end
46
+ assignment = RbsOverloadMatcher.assign(overloads, chunks)
47
+ @rbs_sig_by_chunk = assignment.map {|indexes|
48
+ indexes.filter_map {|i| overloads[i]['segments'] }
49
+ }
50
+ end
51
+
52
+ # 現在のチャンクに割り当てられたオーバーロードの <dt> 行(改行連結)を
53
+ # 返し、チャンクカウンタを進める。メソッド以外のコンパイル
54
+ # (prepare が呼ばれていない)や割り当てが無いチャンクでは nil
55
+ def rbs_signature_dts_for_chunk
56
+ by_chunk = @rbs_sig_by_chunk
57
+ return nil unless by_chunk
58
+ lines = by_chunk[@rbs_chunk_index]
59
+ @rbs_chunk_index += 1
60
+ return nil unless lines && !lines.empty?
61
+ lines.map {|line|
62
+ html = line.map {|kind, text| rbs_segment_html(kind, text) }.join
63
+ %Q(<dt class="rbs-signature"><code>#{html}</code></dt>)
64
+ }.join("\n")
65
+ end
66
+
67
+ private
68
+
69
+ # entry_chunk と同じ規則でシグネチャ行の並び(チャンク)を数える。
70
+ # 属性行({: ...})はシグネチャの並びを切らない
71
+ def rbs_scan_signature_chunks(source)
72
+ chunks = [] #: Array[Array[String]]
73
+ current = nil #: Array[String]?
74
+ source.each_line do |line|
75
+ line = line.chomp
76
+ if (sig = rbs_signature_line(line))
77
+ if current
78
+ current << sig
79
+ else
80
+ fresh = [sig] #: Array[String]
81
+ current = fresh
82
+ chunks << fresh
83
+ end
84
+ elsif current && RDCompiler::METHOD_ATTRIBUTE_LINE_RE =~ line
85
+ # シグネチャの並びを継続
86
+ else
87
+ current = nil
88
+ end
89
+ end
90
+ chunks
91
+ end
92
+
93
+ # シグネチャ行なら "--- " 始まりの正規形にして返す(rd はそのまま)。
94
+ # md のシグネチャ行(### def ...)は MDCompiler がオーバーライドする
95
+ def rbs_signature_line(line)
96
+ line if line.start_with?('---')
97
+ end
98
+
99
+ def rbs_segment_html(kind, text)
100
+ if kind == 'c' and rbs_known_class?(text)
101
+ class_link(text)
102
+ else
103
+ escape_html(text).gsub('-&gt;', '&rarr;')
104
+ end
105
+ end
106
+
107
+ # DB に存在するクラス/モジュールだけリンク化する(自動生成リンクで
108
+ # 死リンクを作らないため。interface や型変数もここで自然に落ちる)。
109
+ # 同じページで同じ型名を何度も引くのでメモ化する
110
+ def rbs_known_class?(name)
111
+ @rbs_known_classes ||= {} #: Hash[String, bool]
112
+ @rbs_known_classes.fetch(name) {
113
+ db = @option[:database]
114
+ @rbs_known_classes[name] =
115
+ begin
116
+ db ? (db.fetch_class(name) && true) : false
117
+ rescue ClassNotFound
118
+ false
119
+ end
120
+ }
121
+ end
122
+
123
+ end
124
+
125
+ end
@@ -15,6 +15,7 @@ require 'bitclust/textutils'
15
15
  require 'bitclust/messagecatalog'
16
16
  require 'bitclust/syntax_highlighter'
17
17
  require 'bitclust/version_badges'
18
+ require 'bitclust/rbs_signatures'
18
19
  require 'rouge'
19
20
  require 'stringio'
20
21
 
@@ -27,6 +28,7 @@ module BitClust
27
28
  include TextUtils
28
29
  include Translatable
29
30
  include VersionBadges
31
+ include RbsSignatures
30
32
 
31
33
  def initialize(urlmapper, hlevel = 1, opt = {})
32
34
  @urlmapper = urlmapper
@@ -59,7 +61,9 @@ module BitClust
59
61
  @opt = opt
60
62
  @type = :method
61
63
  @method = m
62
- setup(m.source, m) {
64
+ source = m.source
65
+ setup(source, m) {
66
+ prepare_rbs_signatures(m, source)
63
67
  entry
64
68
  }
65
69
  ensure
@@ -71,6 +75,7 @@ module BitClust
71
75
  def setup(src, entry = nil)
72
76
  @f = LineInput.new(StringIO.new(src), entry)
73
77
  @out = StringIO.new
78
+ reset_rbs_signatures
74
79
  yield
75
80
  @out.string
76
81
  end
@@ -135,6 +140,12 @@ module BitClust
135
140
  k, v = line.sub(/\A:/, '').split(':', 2)
136
141
  props[k&.strip] = v&.strip
137
142
  end if @type == :method
143
+ # RBS シグネチャは見出し <dt> 群の直後・説明 <dd> の直前に、この
144
+ # チャンクへ振り分けられたぶんだけ <dt> 行として出す(rbs_sig
145
+ # property が無ければ何も出ない)
146
+ if (rbs_dts = rbs_signature_dts_for_chunk())
147
+ @out.puts rbs_dts
148
+ end
138
149
  @out.puts %Q(<dd class="#{@type.to_s}-description">)
139
150
  undef_message if attrs.include?('undef')
140
151
  while @f.next?
@@ -62,6 +62,7 @@ Subcommands(for developers):
62
62
  classes Display defined classes for all ruby.
63
63
  methods Display defined methods for all ruby.
64
64
  methodsince Fill per-name since/until from a version ladder of DBs.
65
+ rbssig Fill RBS type signatures into a 4.0+ DB from .rbs files.
65
66
  checklink Report broken [[c:]]/[[m:]]/[[lib:]]/[[d:]]/[[f:]] links.
66
67
 
67
68
  Subcommands(for packagers):
@@ -110,6 +111,7 @@ Global Options:
110
111
  'classes' => BitClust::Subcommands::ClassesCommand.new,
111
112
  'methods' => BitClust::Subcommands::MethodsCommand.new,
112
113
  'methodsince' => BitClust::Subcommands::MethodsinceCommand.new,
114
+ 'rbssig' => BitClust::Subcommands::RbssigCommand.new,
113
115
  'checklink' => BitClust::Subcommands::ChecklinkCommand.new,
114
116
  }
115
117
  end
@@ -403,6 +403,10 @@ module BitClust
403
403
  @urlmapper.search_url
404
404
  end
405
405
 
406
+ def document_url(name)
407
+ @urlmapper.document_url(name)
408
+ end
409
+
406
410
  def library_index_url
407
411
  @urlmapper.library_index_url
408
412
  end