Class: GemOverrideMarker::SourceExtractor

Inherits:
Object
  • Object
show all
Defined in:
lib/gem_override_marker/source_extractor.rb

Overview

Ruby ソースから対象メソッドの実装を抽出し、インデントを正規化する。

def から end までを正規表現で切り出すことはネストした end の判別が できず不可能なため、Ruby 3.3 標準の prism で AST 解析してメソッド単位で slice する。本家ソース・override の双方を同じ方式で抽出することで、 クラス直下 (本家) と module 多階層 (decorator) のネスト差を吸収できる。

Defined Under Namespace

Classes: MethodNotFoundError

Class Method Summary collapse

Class Method Details

.dedent(code) ⇒ Object

prism の slice は 1行目 (def) が左寄せ・body 行が元のインデントのまま 返るため、body の最小インデントを def の位置まで詰めてネスト差を吸収する。



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/gem_override_marker/source_extractor.rb', line 48

def self.dedent(code)
  lines = code.lines
  return code if lines.empty?

  base_indent = indent_width(lines.first)
  body = lines[1..] || []
  body_indents = body.reject { |line| line.strip.empty? }.map { |line| indent_width(line) }
  min_body_indent = body_indents.min || base_indent

  remove = [ min_body_indent - base_indent, 0 ].max

  result = [ lines.first ]
  result += body.map { |line| line.strip.empty? ? line : line[remove..] }
  result.join
end

.extract(file_path, method_name) ⇒ Object

指定ファイルから対象メソッドの実装を抽出して dedent する。 method_name が nil の場合はファイル全体を返す (view / partial 等)。



15
16
17
18
19
20
21
22
23
# File 'lib/gem_override_marker/source_extractor.rb', line 15

def self.extract(file_path, method_name)
  source = File.read(file_path)
  return dedent(source) if method_name.nil?

  sliced = slice_method(source, method_name.to_sym)
  raise MethodNotFoundError, "method #{method_name} not found in #{file_path}" if sliced.nil?

  dedent(sliced)
end

.indent_width(line) ⇒ Object

行頭の半角スペース数を返す。



65
66
67
# File 'lib/gem_override_marker/source_extractor.rb', line 65

def self.indent_width(line)
  line[/\A */].size
end

.slice_method(source, method_name) ⇒ Object

AST を走査して対象メソッドの DefNode を探し、その範囲を slice する。 最初に見つかった定義を返す。見つからなければ nil。



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/gem_override_marker/source_extractor.rb', line 27

def self.slice_method(source, method_name)
  result = Prism.parse(source)
  found = nil

  visit = lambda do |node|
    return if node.nil? || found

    if node.is_a?(Prism::DefNode) && node.name == method_name
      found = node.location.slice
      return
    end

    node.compact_child_nodes.each { |child| visit.call(child) }
  end

  visit.call(result.value)
  found
end