Skip to content

Commit a650a70

Browse files
znzclaude
andcommitted
メソッド属性行 {: ...} を導入し、説明のための未定義メソッドを区分表示する
Object#to_a などは説明のためにリファレンスに載っているが実際には 定義されていないメソッドで、doctree では #@#nomethod という プリプロセッサコメント(bitclust は読み飛ばす)が使われていたため、 通常のメソッド一覧に混ざって表示されていた(#166)。 シグネチャ見出しの直後に置く kramdown Block IAL 風のメソッド属性行 {: ...} を導入する。@ 始まりのメタデータ(旧 @undef)は GitHub 上で メンションに誤爆するため、この形式に一本化する: - {: nomethod} … 説明のための未定義メソッド。kind = :nomethod になり、 クラスページで「説明のための未定義メソッド」として区分表示される。 @undef と違いページ生成・検索インデックスからは除外しない - {: undef} … 従来の @undef 相当(kind = :undefined)。@undef の パースは削除(使用箇所は doctree 側 PR で同時に移行) - 属性行は「直前のシグネチャ見出し行のみ」に束縛される(kramdown の Block IAL と同じ解釈)。別名(複数シグネチャ)のエントリでは全シグネチャに 同じ属性が必要で、一部にだけ付いていると ParseError(kind はエントリ 単位でしか持てないため)。シグネチャ行と属性行が連続している範囲が ひとつの別名グループになり、次のエントリとは空行で区切る - パーサの属性行探索はシグネチャブロック直後で打ち切り、コード例中の {: ...} を誤検出しない(空行を挟んだ {: ...} はただの本文) - 未知の属性・未対応の key=value(since/until は #132 用に予約)は ParseError にして typo に気づけるようにする - コンパイラ(rd/md)と foreach_method_chunk も属性行をシグネチャ ブロックの一部として扱う。{: undef} は「このメソッドは定義されて いません。」を描画(server 等の動的経路用)、それ以外は無描画 - MethodEntry#description: 先頭のメタデータ段落を説明文として使わない - KIND_NUM に :undefined/:nomethod を追加(sort_key の nil 化を予防) - MARKUP_SPEC に属性行の仕様を追記 Fixes #166 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 1b5feac commit a650a70

21 files changed

Lines changed: 509 additions & 55 deletions

data/bitclust/catalog/ja_JP.UTF-8

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ Thread
110110
スレッド
111111
This reference manual is for a version of Ruby that is no longer maintained.
112112
このマニュアルは既にメンテナンスが終了したバージョンの Ruby を対象としています。
113+
Undefined Methods for Explanation
114+
説明のための未定義メソッド
113115
ancestors
114116
クラス・モジュールの継承リスト
115117
class %s

data/bitclust/template.offline/class

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
[_('Private Instance Methods'), ents.private_instance_methods ],
123123
[_('Protected Instance Methods'),ents.protected_instance_methods ],
124124
[_('Module Functions'), ents.module_functions ],
125+
[_('Undefined Methods for Explanation'), ents.nomethod ],
125126
[_('Constants'), ents.constants ],
126127
[_('Special Variables'), ents.special_variables ,'$']] %>
127128
<%= headline(_("Index")) %>

data/bitclust/template/class

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
[_('Private Instance Methods'), ents.private_instance_methods ],
6363
[_('Protected Instance Methods'),ents.protected_instance_methods ],
6464
[_('Module Functions'), ents.module_functions ],
65+
[_('Undefined Methods for Explanation'), ents.nomethod ],
6566
[_('Constants'), ents.constants ],
6667
[_('Special Variables'), ents.special_variables ]]\
6768
.each do |label, entries|

doc/markdown-samples/MARKUP_SPEC.md

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -429,12 +429,49 @@ nth 番目の要素を返します。
429429
[ReferenceManualFormatDigest](https://github.com/rurema/doctree/wiki/ReferenceManualFormatDigest) に準拠)
430430

431431
1. **シグネチャ**(必須): `### def method(args) -> Type`
432-
2. **要約**(必須): 1段落での概要説明
433-
3. **詳細説明**: 追加の説明文(任意)
434-
4. **param/return/raise**: パラメータ・返り値・例外の説明
435-
5. **注意事項**: 追加の警告など(任意)
436-
6. **使用例**: サンプルコード
437-
7. **SEE**: 関連メソッド参照(リスト形式)
432+
2. **メソッド属性行**(任意): `{: nomethod}` など(下記)
433+
3. **要約**(必須): 1段落での概要説明
434+
4. **詳細説明**: 追加の説明文(任意)
435+
5. **param/return/raise**: パラメータ・返り値・例外の説明
436+
6. **注意事項**: 追加の警告など(任意)
437+
7. **使用例**: サンプルコード
438+
8. **SEE**: 関連メソッド参照(リスト形式)
439+
440+
### メソッド属性行(`{: ...}`
441+
442+
シグネチャ見出しの直後の行に、kramdown の Block IAL に似た属性行を書ける。
443+
`@` プレフィクスのメタデータ行(旧 `@undef`)はメンション誤爆のため廃止し、
444+
この形式に一本化した。
445+
446+
```markdown
447+
### def to_a -> Array
448+
{: nomethod}
449+
450+
オブジェクトを配列に変換した結果を返します。
451+
```
452+
453+
サポートする属性:
454+
455+
- `nomethod` — 説明のためだけに記載されていて実際には定義されていないメソッド
456+
(例: `Object#to_a`)。`kind = :nomethod` になり、クラスページで
457+
「説明のための未定義メソッド」として区分表示される。ページ生成・検索対象には残る
458+
- `undef` — そのクラスで意図的に未定義化されているメソッド
459+
(例: `Complex` における `Comparable` の比較メソッド。旧 `@undef` 相当)。
460+
`kind = :undefined` になり、ページ生成・検索の対象外になる
461+
462+
規則:
463+
464+
- 属性行は**直前のシグネチャ見出し行のみ**に束縛される(kramdown の Block IAL と
465+
同じ解釈)。別名(複数シグネチャ)のエントリで使う場合はすべてのシグネチャ見出しの
466+
直後にそれぞれ付ける。一部にだけ付いているとビルドエラー
467+
(kind はエントリ単位でしか持てないため)
468+
- シグネチャ見出し行と属性行が連続している範囲がひとつのエントリ(別名グループ)に
469+
なる。属性行の直後に別エントリを続ける場合は空行で区切る
470+
- パーサーの属性行探索はシグネチャブロックの直後で打ち切る
471+
(コードブロック中の `{: ...}` 風の行を誤検出しない。空行を挟んだ `{: ...}`
472+
ただの本文)
473+
- 未知の属性はビルドエラー(typo 検出)。`since` / `until` は将来のバージョン情報表示
474+
(バージョン別 DB へのメタデータ付与)のために予約されており、対応するまではエラー
438475

439476
---
440477

lib/bitclust/classentry.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ def add_method(m)
257257
:instance_methods, :private_instance_methods, :protected_instance_methods,
258258
:module_functions,
259259
:constants, :special_variables,
260-
:added, :undefined)
260+
:added, :undefined, :nomethod)
261261

262262
def partitioned_entries(level = 0)
263263
# @type var s: Array[MethodEntry]
@@ -270,12 +270,14 @@ def partitioned_entries(level = 0)
270270
# @type var v: Array[MethodEntry]
271271
# @type var added: Array[MethodEntry]
272272
# @type var undefined: Array[MethodEntry]
273+
# @type var nomethod: Array[MethodEntry]
273274
s = []; spv = []
274275
i = []; ipv = []; ipt = []
275276
mf = []
276277
c = []; v = []
277278
added = []
278279
undefined = []
280+
nomethod = []
279281
entries(level).sort_by(&:name).each do |m|
280282
case m.kind
281283
when :defined, :redefined
@@ -299,10 +301,12 @@ def partitioned_entries(level = 0)
299301
added.push m
300302
when :undefined
301303
undefined.push m
304+
when :nomethod
305+
nomethod.push m
302306
end
303307
end
304308
# steep:ignore:start
305-
Parts.new(s,spv, i,ipv,ipt, mf, c, v, added, undefined)
309+
Parts.new(s,spv, i,ipv,ipt, mf, c, v, added, undefined, nomethod)
306310
# steep:ignore:end
307311
end
308312

lib/bitclust/mdcompiler.rb

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,20 @@ def library_file
9595
def entry_chunk
9696
@out.puts '<dl>' if @option[:force]
9797
first = true
98-
@f.while_match(signature_re) do |line|
99-
method_signature(line, first)
100-
first = false
98+
attrs = [] #: Array[String]
99+
while @f.next?
100+
if signature_re =~ @f.peek
101+
method_signature(@f.gets || raise, first)
102+
first = false
103+
elsif !first && METHOD_ATTRIBUTE_LINE_RE =~ @f.peek
104+
# メソッド属性行は本文には描画しない(undef のみ後でメッセージ)
105+
attrs.concat attribute_tokens(@f.gets)
106+
else
107+
break
108+
end
101109
end
102110
@out.puts %Q(<dd class="#{@type.to_s}-description">)
111+
undef_message if attrs.include?('undef')
103112
while @f.next?
104113
case @f.peek
105114
when signature_re
@@ -131,8 +140,6 @@ def entry_chunk
131140
when /\A@todo\b/
132141
# findings#3: rd 側と同じく行頭アンカー付きで
133142
todo
134-
when /\A@undef\b/
135-
undef_message
136143
when RAW_META_RE
137144
entry_info
138145
when /\A\s*\|/

lib/bitclust/mdparser.rb

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -294,12 +294,26 @@ def read_chunks(f)
294294
end
295295

296296
def read_chunk(f)
297-
header = f.span(SIG_RE)
297+
# シグネチャ行の直後には {: ...} 属性行を置ける(rd 側と同じ)。
298+
# 属性行を挟んでもひとつのチャンク(別名グループ)として読む
299+
header = [] #: Array[String]
300+
sig_lines = [] #: Array[String]
301+
while f.next?
302+
if SIG_RE =~ f.peek
303+
line = f.gets or raise
304+
header.push line
305+
sig_lines.push line
306+
elsif !sig_lines.empty? && METHOD_ATTRIBUTE_LINE_RE =~ f.peek
307+
header.push(f.gets || raise)
308+
else
309+
break
310+
end
311+
end
298312
body = md_break(f)
299313
src = (header + body).join('')
300-
src.location = header[0].location
301-
sigs = header.map {|line| method_signature(line) }
302-
mainsig = check_chunk_signatures(sigs, header[0])
314+
src.location = sig_lines[0].location
315+
sigs = sig_lines.map {|line| method_signature(line) }
316+
mainsig = check_chunk_signatures(sigs, sig_lines[0])
303317
names = sigs.map {|s| s.name }.compact.uniq.sort
304318
Chunk.new(mainsig, names, src)
305319
end

lib/bitclust/methodentry.rb

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ def <=>(other)
4444
sort_key() <=> other.sort_key
4545
end
4646

47-
KIND_NUM = {:defined => 0, :redefined => 1, :added => 2}
47+
KIND_NUM = {:defined => 0, :redefined => 1, :added => 2,
48+
:undefined => 3, :nomethod => 4}
4849

4950
def sort_key
5051
[label(), KIND_NUM[kind()]]
@@ -99,7 +100,7 @@ def klass
99100
persistent_properties {
100101
property :names, '[String]'
101102
property :visibility, 'Symbol' ## :public | :private | :protected
102-
property :kind, 'Symbol' ## :defined | :added | :redefined | :undefined
103+
property :kind, 'Symbol' ## :defined | :added | :redefined | :undefined | :nomethod
103104
property :source, 'String'
104105
property :source_location, 'Location'
105106
}
@@ -215,8 +216,17 @@ def undefined?
215216
kind() == :undefined
216217
end
217218

219+
# 説明のためだけに記載されていて実際には定義されていないメソッド
220+
# (ソースの @nomethod メタデータ行で指定する)
221+
def nomethod?
222+
kind() == :nomethod
223+
end
224+
218225
def description
219-
description_text(source.split(/\n\n+/, 3)[1])
226+
paragraphs = source.split(/\n\n+/).drop(1)
227+
# {: ...} 属性行や @param などのメタデータ段落は説明文として使わない
228+
para = paragraphs.find {|p| !p.start_with?('@', '{:') } || paragraphs.first || ''
229+
description_text(para)
220230
end
221231
end
222232
end

lib/bitclust/rdcompiler.rb

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,19 +109,31 @@ def entry
109109
end
110110
end
111111

112+
# シグネチャ行に続くメソッド属性行({: ...}。直前のシグネチャ行に束縛)
113+
METHOD_ATTRIBUTE_LINE_RE = /\A\{:.*\}[ \t]*$/
114+
112115
def entry_chunk
113116
@out.puts '<dl>' if @option[:force]
114117
first = true
115-
@f.while_match(/\A---/) do |line|
116-
method_signature(line, first)
117-
first = false
118+
attrs = [] #: Array[String]
119+
while @f.next?
120+
if /\A---/ =~ @f.peek
121+
method_signature(@f.gets || raise, first)
122+
first = false
123+
elsif !first && METHOD_ATTRIBUTE_LINE_RE =~ @f.peek
124+
# メソッド属性行は本文には描画しない(undef のみ後でメッセージ)
125+
attrs.concat attribute_tokens(@f.gets)
126+
else
127+
break
128+
end
118129
end
119130
props = {} #: Hash[String?, String?]
120131
@f.while_match(/\A:/) do |line|
121132
k, v = line.sub(/\A:/, '').split(':', 2)
122133
props[k&.strip] = v&.strip
123134
end if @type == :method
124135
@out.puts %Q(<dd class="#{@type.to_s}-description">)
136+
undef_message if attrs.include?('undef')
125137
while @f.next?
126138
case @f.peek
127139
when /\A===+/
@@ -150,8 +162,6 @@ def entry_chunk
150162
see
151163
when /\A@todo\b/
152164
todo
153-
when /\A@undef\b/
154-
undef_message
155165
when /\A@[a-z]/
156166
entry_info
157167
else
@@ -352,11 +362,14 @@ def todo
352362
line '</p>'
353363
end
354364

355-
# findings#2: @undef の専用描画。entry_info の else 枝に落として
356-
# 内部用語 [UNKNOWN_META_INFO] を出さない(Complex の比較演算子等。
357-
# statichtml は undefined エントリを skip するので server 等の動的経路用)
365+
def attribute_tokens(attr_line)
366+
(attr_line || '')[/\A\{:(.*)\}/, 1].to_s.strip.split(/\s+/)
367+
end
368+
369+
# {: undef} 属性付きエントリのメッセージ。ページを見に来た人向け
370+
# (statichtml は undefined エントリを skip するので server 等の動的経路用)。
371+
# nomethod は説明が本文に書かれている前提のマーカーなので何も描画しない
358372
def undef_message
359-
@f.gets
360373
line '<p>'
361374
line 'このメソッドは定義されていません。'
362375
line '</p>'

lib/bitclust/rrdparser.rb

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -269,13 +269,30 @@ def read_chunks(f)
269269
result
270270
end
271271

272+
# シグネチャ行に続くメソッド属性行({: ...}。直前のシグネチャ行に束縛)
273+
METHOD_ATTRIBUTE_LINE_RE = /\A\{:.*\}[ \t]*$/
274+
272275
def read_chunk(f)
273-
header = f.span(/\A---/)
276+
# シグネチャ行の直後には {: ...} 属性行を置ける。属性行を挟んでも
277+
# ひとつのチャンク(別名グループ)として読む
278+
header = [] #: Array[String]
279+
sig_lines = [] #: Array[String]
280+
while f.next?
281+
if /\A---/ =~ f.peek
282+
line = f.gets or raise
283+
header.push line
284+
sig_lines.push line
285+
elsif !sig_lines.empty? && METHOD_ATTRIBUTE_LINE_RE =~ f.peek
286+
header.push(f.gets || raise)
287+
else
288+
break
289+
end
290+
end
274291
body = f.break(/\A(?:---|={1,2}[^=])/)
275292
src = (header + body).join('')
276-
src.location = header[0].location
277-
sigs = header.map {|line| method_signature(line) }
278-
mainsig = check_chunk_signatures(sigs, header[0])
293+
src.location = sig_lines[0].location
294+
sigs = sig_lines.map {|line| method_signature(line) }
295+
mainsig = check_chunk_signatures(sigs, sig_lines[0])
279296
names = sigs.map {|s| s.name }.compact.uniq.sort
280297
Chunk.new(mainsig, names, src)
281298
end
@@ -462,9 +479,16 @@ def signature
462479

463480
def define_method(chunk)
464481
id = method_id(chunk)
482+
attrs = method_attributes(chunk)
465483
@db.open_method(id) {|m|
466484
m.names = chunk.names.sort
467-
m.kind = chunk.source.match?(/^@undef$/) ? :undefined : @kind
485+
m.kind = if attrs.include?('undef')
486+
:undefined
487+
elsif attrs.include?('nomethod')
488+
:nomethod
489+
else
490+
@kind
491+
end
468492
# steep:ignore:start
469493
m.visibility = @visibility || :public
470494
# steep:ignore:end
@@ -477,6 +501,44 @@ def define_method(chunk)
477501
}
478502
end
479503

504+
# 現在サポートするメソッド属性({: ...} 属性行のトークン)。
505+
# since/until は将来のバージョン情報表示(#132)用に予約
506+
METHOD_ATTRIBUTES = %w[nomethod undef]
507+
508+
# kramdown Block IAL 風の {: ...} 属性行からメタデータを集める。
509+
# 属性行は「直前のシグネチャ行のみ」に束縛される(kramdown と同じ解釈)。
510+
# kind はエントリ単位でしか持てないので、別名(複数シグネチャ)の
511+
# エントリでは全シグネチャに同じ属性が付いていることを要求する。
512+
# 本文に入ったら探索を打ち切る(コード例中の {: ...} を誤検出しないため)
513+
def method_attributes(chunk)
514+
per_sig = [] #: Array[Array[String]]
515+
chunk.source.each_line do |line_|
516+
line = line_.chomp
517+
case line
518+
when /\A---\s/, /\A\#\#\#\s/
519+
per_sig.push []
520+
when /\A\{:(.*)\}[ \t]*\z/
521+
break if per_sig.empty?
522+
($1 || raise).strip.split(/\s+/).each do |token|
523+
unless METHOD_ATTRIBUTES.include?(token)
524+
raise ParseError,
525+
"#{chunk.source.location}: unknown method attribute #{token.inspect} (supported: #{METHOD_ATTRIBUTES.join(', ')})"
526+
end
527+
per_sig.last&.push token
528+
end
529+
else
530+
break
531+
end
532+
end
533+
return [] if per_sig.empty?
534+
sets = per_sig.map {|a| a.uniq.sort }
535+
unless sets.uniq.size == 1
536+
raise ParseError,
537+
"#{chunk.source.location}: method attributes must be the same on every signature of an entry: #{sets.inspect}"
538+
end
539+
sets.first || raise
540+
end
541+
480542
def method_id(chunk)
481543
id = MethodID.new
482544
id.library = @library

0 commit comments

Comments
 (0)