Module: BitClust::EntitySplitter

Defined in:
lib/bitclust/entity_splitter.rb

Overview

マルチエンティティ RD のエンティティ単位分割(O3)。

「ヘッダ関係(include/extend/alias)を持つエンティティは自分のファイルを持ち、 関係は front matter に一元化する」ため、関係を持つマルチエンティティファイルを エンティティ単位のセグメントへ分割する。関係を持たない束ね(Errno 族等)は 分割しない(判断はオーケストレータが行う。ここは純テキスト変換)。

分割の前処理として、スコープ定数(スコープ内で常に真/偽)の版ゲートのうち エンティティ H1 を包むものを解決する。版で改名されたエンティティ (thread/Mutex, Net::HTTPURITooLong 等の #@since/#@else H1 ペア)が スコープ内の単一 H1 に収束する(旧名は活き枝の alias 行として残っている)。

Constant Summary collapse

H1_RE =
/\A=(?!=)\s*(?:class|module|object|reopen|redefine)\s+(\S+)/
GATE_OPEN_RE =
/\A\#@(since|until|if)\s*(.*)$/
BLOCK_OPEN_RE =
/\A\#@(?:since|until|if|samplecode)\b/
BLANK_RE =
/\A\s*\z/

Class Method Summary collapse

Class Method Details

.constant_truth(line, scope) ⇒ Object

ゲート行のスコープ定数評価: 常真なら true、常偽なら false、 定数でない/ゲートでないなら nil。 #@if は「version >= "X"(X <= 下限)」形のみ常真と証明できる



58
59
60
61
62
63
64
65
66
67
68
# File 'lib/bitclust/entity_splitter.rb', line 58

def constant_truth(line, scope)
  cond = gate_condition(line)
  return nil unless cond
  if cond.kind == :if
    WholeFileGate.provably_true_if?(cond.version, scope) ? true : nil
  elsif scope.always?(cond)
    true
  elsif scope.never?(cond)
    false
  end
end

.contains_entity_h1?(lines) ⇒ Boolean

samplecode の内容を除いて、エンティティ H1 行を含むか

Returns:

  • (Boolean)


71
72
73
74
75
76
77
78
79
80
81
# File 'lib/bitclust/entity_splitter.rb', line 71

def contains_entity_h1?(lines)
  code = 0
  lines.each do |l|
    case l
    when /\A\#@samplecode\b/ then code += 1
    when /\A\#@end\b/ then code -= 1 if code > 0
    when H1_RE then return true if code.zero?
    end
  end
  false
end

.entity_filename(name) ⇒ Object

エンティティ名 → 出力ファイル名(拡張子なし)。既存の命名規約に合わせ

→ __



150
151
152
# File 'lib/bitclust/entity_splitter.rb', line 150

def entity_filename(name)
  name.gsub('::', '__')
end

.first_content_is_h1?(branch_lines) ⇒ Boolean

Returns:

  • (Boolean)


210
211
212
213
# File 'lib/bitclust/entity_splitter.rb', line 210

def first_content_is_h1?(branch_lines)
  first = branch_lines.find { |l| l !~ BLANK_RE }
  first =~ H1_RE ? true : false
end

.first_h1_name(lines, i) ⇒ Object

i 以降の最初の非空行が H1 ならその名前を返す



216
217
218
219
# File 'lib/bitclust/entity_splitter.rb', line 216

def first_h1_name(lines, i)
  first = (lines[i..] || raise).find { |l| l !~ BLANK_RE }
  first =~ H1_RE ? $1 : nil
end

.gate_condition(line) ⇒ Object



172
173
174
175
176
# File 'lib/bitclust/entity_splitter.rb', line 172

def gate_condition(line)
  return nil unless line =~ GATE_OPEN_RE
  # Preprocessor は #@since "1.8.5" のクォート形式も受理する
  IncludeGraph::Condition.new(($1 || raise).to_sym, ($2 || raise).strip.delete_prefix('"').delete_suffix('"'))
end

.gate_wraps_segment?(lines, start, stop) ⇒ Boolean

start のゲート開き行に対応する #@end がセグメント終端(末尾の空行は許容)に あるか。ゲートがセグメント全体を包むときのみ真

Returns:

  • (Boolean)


131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/bitclust/entity_splitter.rb', line 131

def gate_wraps_segment?(lines, start, stop)
  depth = 0
  close = nil #: Integer?
  (start...stop).each do |i|
    case lines[i]
    when BLOCK_OPEN_RE then depth += 1
    when /\A\#@end\b/
      depth -= 1
      if depth.zero?
        close = i
        break
      end
    end
  end
  return false unless close
  (lines[(close + 1)...stop] || raise).all? { |l| l =~ BLANK_RE }
end

.header_relations?(src) ⇒ Boolean

いずれかのエンティティの H1 直後のヘッダ領域(関係行・#@・空行が続く範囲)に include/extend/alias があるか。本文中やコード例の同名行は数えない

Returns:

  • (Boolean)


156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/bitclust/entity_splitter.rb', line 156

def header_relations?(src)
  in_header = false
  src.each_line do |line|
    if line =~ H1_RE
      in_header = true
    elsif in_header
      case line
      when /\A(?:include|extend|alias)\s+\S/ then return true
      when /\A\#@/, BLANK_RE then nil
      else in_header = false
      end
    end
  end
  false
end

.parse_block(lines, i) ⇒ Object

i のゲート開始行から対応を取り、else_body:, next: を返す。 body/else_body は枝の行列、next は #@end の次の行 index。対応が取れなければ nil



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/bitclust/entity_splitter.rb', line 180

def parse_block(lines, i)
  depth = 0
  body = [] #: Array[String]
  else_body = [] #: Array[String]
  current = body
  j = i
  while j < lines.length
    line = lines[j]
    case line
    when BLOCK_OPEN_RE
      depth += 1
      current << line unless j == i
    when /\A\#@else\b/
      if depth == 1
        current = else_body
      else
        current << line
      end
    when /\A\#@end\b/
      depth -= 1
      return { body: body, else_body: else_body, next: j + 1 } if depth.zero?
      current << line
    else
      current << line
    end
    j += 1
  end
  nil
end

.resolve_header_gates(src, scope) ⇒ Object

スコープ定数(スコープ内で常に真/偽)な版ゲートのうち、エンティティ H1 を 含むブロックを解決する。活きている枝の内容(再帰的に解決)を残し、 ゲート行と死んだ枝を落とす。スコープ内で真偽が変わるゲートと、 H1 を含まないゲート(散文の版分岐・gated relations)は触らない。



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/bitclust/entity_splitter.rb', line 30

def resolve_header_gates(src, scope)
  lines = src.lines
  out = [] #: Array[String]
  i = 0
  while i < lines.length
    line = lines[i]
    truth = constant_truth(line, scope)
    if !truth.nil? && (block = parse_block(lines, i)) &&
       contains_entity_h1?(block[:body] + block[:else_body])
      active = truth ? block[:body] : block[:else_body]
      resolved = resolve_header_gates(active.join, scope).lines
      if resolved.empty? && out.last =~ BLANK_RE && lines[block[:next]] =~ BLANK_RE
        i = block[:next] + 1   # ブロックごと消えた場合は後続の空行を1つ畳む
      else
        i = block[:next]
      end
      out.concat(resolved)
    else
      out << line
      i += 1
    end
  end
  out.join
end

.segments(src) ⇒ Object

エンティティ単位のセグメント列 [[エンティティ名, テキスト], ...] を返す。 境界は深さ0の H1 行、または最初の内容行が H1 の版ゲートブロック (スコープ内ゲート付きエンティティ。ブロック全体が1セグメント)。 先頭に H1 以外の内容がある場合(ライブラリ概要部)は name=nil の ベースセグメントとして返す。連結すると入力に一致する。



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/bitclust/entity_splitter.rb', line 88

def segments(src)
  lines = src.lines
  # [行index, エンティティ名, ゲート付きか]
  boundaries = [] #: Array[[Integer, String?, bool]]
  depth = 0
  lines.each_with_index do |line, i|
    case line
    when BLOCK_OPEN_RE
      if depth.zero? && line =~ /\A\#@(?:since|until|if)\b/ &&
         (name = first_h1_name(lines, i + 1))
        boundaries << [i, name, true]
      end
      depth += 1
    when /\A\#@end\b/
      depth -= 1
    when H1_RE
      boundaries << [i, $1, false] if depth.zero?
    end
  end
  # ゲート付き H1 境界は、ゲートがセグメント全体を包む場合のみ分割境界にする。
  # H1 と見出しだけがゲートされ内容がゲートの外にある形(syslog の旧構造)を
  # 単独ファイル化すると、ゲートが偽の版で自立パースできない
  # (旧世界では直前エンティティの内容として付く)ため、直前セグメントに残す
  boundaries = boundaries.each_with_index.select { |(start, _, gated), idx|
    next true unless gated
    stop = idx + 1 < boundaries.length ? boundaries[idx + 1][0] : lines.length
    gate_wraps_segment?(lines, start, stop)
  }.map { |(start, name, _), _| [start, name] } #: Array[[Integer, String?]]
  return nil if boundaries.empty?
  if (lines[0...(boundaries.first || raise)[0]] || raise).all? { |l| l =~ BLANK_RE }
    boundaries[0] = [0, boundaries[0][1]]   # 先頭の空行は最初のセグメントへ
  else
    boundaries.unshift([0, nil])            # ライブラリ概要部(ベースセグメント)
  end

  boundaries.each_with_index.map do |(start, name), idx|
    stop = idx + 1 < boundaries.length ? boundaries[idx + 1][0] : lines.length
    [name, (lines[start...stop] || raise).join]
  end
end