Module: CrystalRuby::SourceReader

Defined in:
lib/crystalruby/source_reader.rb

Class Method Summary collapse

Class Method Details

.extract_args_and_source_from_method(method, raw: false) ⇒ Object

Given a method, extracts the source code of the block passed to it and also converts any keyword arguments given in the method definition as a named map of keyword names to Crystal types. Also supports basic ffi symbol types.

E.g.

def add a: Int32 | Int64, b: :int

The above will be converted to:

a: Int32 | Int64, # Int32 | Int64 is a Crystal type
b: :int           # :int is an FFI type shorthand

If raw is true, the source is expected to be Raw Crystal code captured in a string or Heredoc literal. Otherwise the Ruby code (assumed to be valid Crystal) is extracted.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/crystalruby/source_reader.rb', line 68

def extract_args_and_source_from_method(method, raw: false)
  method_source = extract_expr_from_source_location(method.source_location)
  parsed_source = Prism.parse(method_source).value
  params = search_node(parsed_source, Prism::ParametersNode)
  args = params ? params.keywords.map { |kw| [kw.name, node_to_s(kw.value)] }.to_h : {}
  body_node = parsed_source.statements.body[0].body
  if body_node.respond_to?(:rescue_clause) && body_node.rescue_clause
    wrapped = %(begin\n#{body_node.statements.slice}\n#{body_node.rescue_clause.slice}\nend)
    body_node = Prism.parse(wrapped).value
  end
  body = raw ? extract_raw_string_node(body_node) : node_to_s(body_node)

  args.transform_values! do |type_exp|
    if CrystalRuby::Typemaps::CRYSTAL_TYPE_MAP.key?(type_exp[1..-1].to_sym)
      type_exp[1..-1].to_sym
    else
      TypeBuilder.build_from_source(type_exp, context: method.owner)
    end
  end.to_h
  [args, body]
end

.extract_expr_from_source_location(source_location) ⇒ Object

Reads code line by line from a given source location and returns the first valid Ruby expression found



6
7
8
9
10
11
12
13
14
# File 'lib/crystalruby/source_reader.rb', line 6

def extract_expr_from_source_location(source_location)
  lines = source_location.then { |f, l| IO.readlines(f)[l - 1..] }
  lines[0] = lines[0][/CRType.*/] if lines[0] =~ /<\s+CRType/ || lines[0] =~ /= CRType/
  lines.each.with_object([]) do |line, expr_source|
    break expr_source.join("") if Prism.parse((expr_source << line).join("")).success?
  end
rescue StandardError
  raise "Failed to extract expression from source location: #{source_location}. Ensure the file exists and the line number is correct. Extraction from a REPL is not supported"
end

.extract_raw_string_node(node) ⇒ Object



37
38
39
40
41
42
# File 'lib/crystalruby/source_reader.rb', line 37

def extract_raw_string_node(node)
  search_node(node, Prism::InterpolatedStringNode)&.parts&.map do |p|
    p.respond_to?(:unescaped) ? p.unescaped : p.slice
  end&.join("") ||
    search_node(node, Prism::StringNode).unescaped
end

.extract_source_from_proc(block, raw: false) ⇒ Object

Given a proc, extracts the source code of the block passed to it If raw is true, the source is expected to be Raw Crystal code captured in a string or Heredoc literal. Otherwise the Ruby code (assumed to be valid Crystal) is extracted.



26
27
28
29
30
31
32
33
34
35
# File 'lib/crystalruby/source_reader.rb', line 26

def extract_source_from_proc(block, raw: false)
  block_source = extract_expr_from_source_location(block.source_location)
  parsed_source = Prism.parse(block_source).value

  node = parsed_source.statements.body[0].arguments&.arguments&.find { |x| search_node(x, Prism::StatementsNode) }
  node ||= parsed_source.statements.body[0]
  body_node = search_node(node, Prism::StatementsNode)

  raw ? extract_raw_string_node(body_node) : node_to_s(body_node)
end

.node_to_s(node) ⇒ Object

Simple helper function to turn a SyntaxTree node back into a Ruby string The default formatter will turn a break/return of [1,2,3] into a brackless 1,2,3 Can’t have that in Crystal as it turns it into a Tuple



47
48
49
# File 'lib/crystalruby/source_reader.rb', line 47

def node_to_s(node)
  node&.slice || ""
end

.search_node(result, node_type) ⇒ Object



16
17
18
19
20
# File 'lib/crystalruby/source_reader.rb', line 16

def search_node(result, node_type)
  result.breadth_first_search do |node|
    node_type === node
  end
end