diff --git a/doc/markdown-samples/MARKUP_SPEC.md b/doc/markdown-samples/MARKUP_SPEC.md index 2e01010..ceb210b 100644 --- a/doc/markdown-samples/MARKUP_SPEC.md +++ b/doc/markdown-samples/MARKUP_SPEC.md @@ -458,6 +458,11 @@ nth 番目の要素を返します。 - `undef` — そのクラスで意図的に未定義化されているメソッド (例: `Complex` における `Comparable` の比較メソッド。旧 `@undef` 相当)。 `kind = :undefined` になり、ページ生成・検索の対象外になる +- `since="X"` / `until="X"` — このシグネチャの名前が Ruby X から存在する + (`since`)/ Ruby X で削除される(`until`。半開区間: X 未満のバージョンでは + 存在し、X 以降には存在しない)ことを明示する。X は `"3.2"` のように数字と + ドットのみをダブルクォートで囲んで書く。シグネチャ見出しの横に + since/until バッジとして表示される 規則: @@ -465,13 +470,30 @@ nth 番目の要素を返します。 同じ解釈)。別名(複数シグネチャ)のエントリで使う場合はすべてのシグネチャ見出しの 直後にそれぞれ付ける。一部にだけ付いているとビルドエラー (kind はエントリ単位でしか持てないため) +- ただし `since=`/`until=` は `nomethod`/`undef` と違い**シグネチャ単位**の + 値であり、別名ごとに異なるバージョンを持ってよい(全シグネチャで揃える必要は + ない)。追加/削除時期が別名ごとに違う場合が本来の用途: + + ```markdown + ### def -@ -> object + {: since="2.0.0"} + ### def dedup -> object + {: since="3.0"} + ``` + + (本体 `-@` は Ruby 2.0.0 から、別名 `dedup` はそれより後の Ruby 3.0 から、 + という例。それぞれのシグネチャ見出しの横に別々の since バッジが付く) - シグネチャ見出し行と属性行が連続している範囲がひとつのエントリ(別名グループ)に なる。属性行の直後に別エントリを続ける場合は空行で区切る - パーサーの属性行探索はシグネチャブロックの直後で打ち切る (コードブロック中の `{: ...}` 風の行を誤検出しない。空行を挟んだ `{: ...}` は ただの本文) -- 未知の属性はビルドエラー(typo 検出)。`since` / `until` は将来のバージョン情報表示 - (バージョン別 DB へのメタデータ付与)のために予約されており、対応するまではエラー +- 未知の属性・不正な形式(`since=X` のような非引用値、`since=""` のような + 空値、数字とドット以外を含む値、未知のキーなど)はビルドエラー(typo 検出) +- 明示された `since`/`until` は、全バージョンの DB を横断して自動算出する + `bitclust methodsince` サブコマンドの結果より常に優先される(算出側は既に + 値がある名前を上書きしない)。主な用途は自動算出が届かない 1.8.7 以前からの + 存在(フロア救済)や、算出結果の補正 --- diff --git a/lib/bitclust/rrdparser.rb b/lib/bitclust/rrdparser.rb index eacff76..6407ee6 100644 --- a/lib/bitclust/rrdparser.rb +++ b/lib/bitclust/rrdparser.rb @@ -12,6 +12,7 @@ require 'bitclust/preprocessor' require 'bitclust/methodid' require 'bitclust/methoddatabase' +require 'bitclust/methodsignature' require 'bitclust/lineinput' require 'bitclust/parseutils' require 'bitclust/nameutils' @@ -482,9 +483,9 @@ def define_method(chunk) attrs = method_attributes(chunk) @db.open_method(id) {|m| m.names = chunk.names.sort - m.kind = if attrs.include?('undef') + m.kind = if attrs.flags.include?('undef') :undefined - elsif attrs.include?('nomethod') + elsif attrs.flags.include?('nomethod') :nomethod else @kind @@ -494,6 +495,10 @@ def define_method(chunk) # steep:ignore:end m.source = chunk.source m.source_location = chunk.source.location + attrs.since_until.each do |name, kv| + m.fill_since(name, kv['since'] || raise) if kv['since'] + m.fill_until(name, kv['until'] || raise) if kv['until'] + end case @kind when :added, :redefined @library.add_method m @@ -502,42 +507,122 @@ def define_method(chunk) end # 現在サポートするメソッド属性({: ...} 属性行のトークン)。 - # since/until は将来のバージョン情報表示(#132)用に予約 + # - nomethod/undef: 裸語。kind はエントリ単位でしか持てないので、 + # 別名(複数シグネチャ)のエントリでは全シグネチャに同じ属性が + # 付いていることを要求する + # - since="X"/until="X"(bitclust#132 P4): シグネチャ単位で束縛され、 + # そのシグネチャの名前だけに適用される。nomethod/undef と違い、 + # 別名ごとに異なる値を持てる(全シグネチャ一致は要求しない)のが + # 本来の用途。X は "3.2" のように数字とドットのみ METHOD_ATTRIBUTES = %w[nomethod undef] + KV_METHOD_ATTRIBUTES = %w[since until] + KV_METHOD_ATTRIBUTE_VALUE_RE = /\A\d+(?:\.\d+)*\z/ + + # md の `### def name ...`/`### module_function def name ...`/ + # `### const name`/`### gvar $name` シグネチャ行を rd 形式 + # (`--- name ...`)へ正規化するための接頭辞(MDParser::SIG_RE と + # 同じパターン)。属性の紐付け先となるメソッド名を + # MethodSignature.parse で取り出すために使う + MD_METHOD_SIG_PREFIX_RE = /\A### (?:module_function def |def |const |gvar )/ + + # method_attributes の返り値。 + # - flags: 裸語トークン(nomethod/undef)の集合。エントリ単位の属性 + # なので全シグネチャで同一であることを method_attributes が保証済み + # - since_until: シグネチャ単位で束縛された since="X"/until="X" を + # { メソッド名 => { "since" => "X", "until" => "X" } } の形で保持する + MethodAttributes = Struct.new(:flags, :since_until) # kramdown Block IAL 風の {: ...} 属性行からメタデータを集める。 # 属性行は「直前のシグネチャ行のみ」に束縛される(kramdown と同じ解釈)。 - # kind はエントリ単位でしか持てないので、別名(複数シグネチャ)の - # エントリでは全シグネチャに同じ属性が付いていることを要求する。 + # 裸語トークン(nomethod/undef)は kind がエントリ単位でしか持てないため + # 全シグネチャで同じであることを要求するが、since=/until= はシグネチャ + # 単位でそのままの名前に適用するため、この一致要求からは除外する。 # 本文に入ったら探索を打ち切る(コード例中の {: ...} を誤検出しないため) def method_attributes(chunk) per_sig = [] #: Array[Array[String]] + since_until = {} #: Hash[String, Hash[String, String]] + current_name = nil #: String? + current_keys = nil #: Hash[String, bool]? chunk.source.each_line do |line_| line = line_.chomp case line when /\A---\s/, /\A\#\#\#\s/ per_sig.push [] + current_name = method_attribute_target_name(line) + current_keys = {} when /\A\{:(.*)\}[ \t]*\z/ break if per_sig.empty? ($1 || raise).strip.split(/\s+/).each do |token| - unless METHOD_ATTRIBUTES.include?(token) - raise ParseError, - "#{chunk.source.location}: unknown method attribute #{token.inspect} (supported: #{METHOD_ATTRIBUTES.join(', ')})" + if METHOD_ATTRIBUTES.include?(token) + per_sig.last&.push token + else + key, value = parse_kv_method_attribute(token, chunk) + keys = current_keys or raise + if keys.key?(key) + raise ParseError, + "#{chunk.source.location}: duplicate method attribute #{key.inspect} on the same signature" + end + keys[key] = true + name = current_name or + raise ParseError, + "#{chunk.source.location}: cannot determine the signature name to bind method attribute #{token.inspect} to" + (since_until[name] ||= {})[key] = value end - per_sig.last&.push token end else break end end - return [] if per_sig.empty? + return MethodAttributes.new([], {}) if per_sig.empty? sets = per_sig.map {|a| a.uniq.sort } unless sets.uniq.size == 1 raise ParseError, "#{chunk.source.location}: method attributes must be the same on every signature of an entry: #{sets.inspect}" end - sets.first || raise - end + # 紐付け先の名前がエントリの names と食い違ったら黙って捨てずに + # エラーにする(名前導出の規約ずれを CI で検出するための安全網) + since_until.each_key do |name| + unless chunk.names.include?(name) + raise ParseError, + "#{chunk.source.location}: since/until attribute bound to unknown signature name #{name.inspect} (entry names: #{chunk.names.join(', ')})" + end + end + MethodAttributes.new(sets.first || raise, since_until) + end + + # since="X"/until="X" 形式のトークンを解析して [key, value] を返す。 + # 受理する形式以外(キーが不明・値が非引用・値が数字とドット以外を + # 含む等)はすべて ParseError にする + def parse_kv_method_attribute(token, chunk) + m = /\A(\w+)=(.*)\z/.match(token) + if m + key = m[1] || raise + raw_value = m[2] || raise + quoted = /\A"(.*)"\z/.match(raw_value) + if quoted && KV_METHOD_ATTRIBUTES.include?(key) && KV_METHOD_ATTRIBUTE_VALUE_RE.match?(quoted[1] || raise) + return [key, (quoted[1] || raise)] + end + end + raise ParseError, + "#{chunk.source.location}: invalid method attribute #{token.inspect} " \ + "(supported: #{METHOD_ATTRIBUTES.join(', ')}, " \ + "#{KV_METHOD_ATTRIBUTES.map {|k| %Q(#{k}="X") }.join('/')} where X is digits and dots, e.g. since=\"3.2\")" + end + private :parse_kv_method_attribute + + # シグネチャ行(rd の `--- ...` / md の `### def ...` 等)から、 + # 属性の紐付け先となるメソッド名を取り出す。 + # 特殊変数のシグネチャ名は "$SAFE" のように $ 付きだが、エントリの + # names は先頭の $ を除いた形("SAFE"。"$$" なら "$")で格納される + # (method_signature の Signature.new(nil, '$', name[1..-1]) と同じ規約) + # ため、since_by_name のキーも names に合わせて $ を1つ剥がす + def method_attribute_target_name(line) + normalized = line.sub(MD_METHOD_SIG_PREFIX_RE, '--- ') + MethodSignature.parse(normalized).name.sub(/\A\$/, '') + rescue ParseError + nil + end + private :method_attribute_target_name def method_id(chunk) id = MethodID.new diff --git a/lib/bitclust/version_badges.rb b/lib/bitclust/version_badges.rb index bd154cc..2c1f0fc 100644 --- a/lib/bitclust/version_badges.rb +++ b/lib/bitclust/version_badges.rb @@ -32,6 +32,7 @@ module VersionBadges # signature's
$0[permalink][rdoc]$PROGRAM_NAMERuby 1.9.1 から+bar +
+