lib/yarp.rb in yarp-0.6.0 vs lib/yarp.rb in yarp-0.7.0
- old
+ new
@@ -5,11 +5,11 @@
# conjunction with locations to allow them to resolve line numbers and source
# ranges.
class Source
attr_reader :source, :offsets
- def initialize(source, offsets)
+ def initialize(source, offsets = compute_offsets(source))
@source = source
@offsets = offsets
end
def slice(offset, length)
@@ -21,10 +21,18 @@
end
def column(value)
value - offsets[line(value) - 1]
end
+
+ private
+
+ def compute_offsets(code)
+ offsets = [0]
+ code.b.scan("\n") { offsets << $~.end(0) }
+ offsets
+ end
end
# This represents a location in the source.
class Location
# A Source object that is used to determine more information from the given
@@ -99,10 +107,12 @@
end
end
# This represents a comment that was encountered during parsing.
class Comment
+ TYPES = [:inline, :embdoc, :__END__]
+
attr_reader :type, :location
def initialize(type, location)
@type = type
@location = location
@@ -224,10 +234,16 @@
newline_marked = Array.new(1 + @source.offsets.size, false)
visitor = MarkNewlinesVisitor.new(newline_marked)
value.accept(visitor)
value
end
+
+ # Construct a new ParseResult with the same internal values, but with the
+ # given source.
+ def with_source(source)
+ ParseResult.new(value, comments, errors, warnings, source)
+ end
end
# This represents a token from the Ruby source.
class Token
attr_reader :type, :value, :location
@@ -277,10 +293,15 @@
newline_marked[line] = true
@newline = true
end
end
+ # Slice the location of the node from the source.
+ def slice
+ location.slice
+ end
+
def pretty_print(q)
q.group do
q.text(self.class.name.split("::").last)
location.pretty_print(q)
q.text("[Li:#{location.start_line}]") if newline?
@@ -304,10 +325,150 @@
end
# This module is used for testing and debugging and is not meant to be used by
# consumers of this library.
module Debug
+ class ISeq
+ attr_reader :parts
+
+ def initialize(parts)
+ @parts = parts
+ end
+
+ def type
+ parts[0]
+ end
+
+ def local_table
+ parts[10]
+ end
+
+ def instructions
+ parts[13]
+ end
+
+ def each_child
+ instructions.each do |instruction|
+ # Only look at arrays. Other instructions are line numbers or
+ # tracepoint events.
+ next unless instruction.is_a?(Array)
+
+ instruction.each do |opnd|
+ # Only look at arrays. Other operands are literals.
+ next unless opnd.is_a?(Array)
+
+ # Only look at instruction sequences. Other operands are literals.
+ next unless opnd[0] == "YARVInstructionSequence/SimpleDataFormat"
+
+ yield ISeq.new(opnd)
+ end
+ end
+ end
+ end
+
+ # For the given source, compiles with CRuby and returns a list of all of the
+ # sets of local variables that were encountered.
+ def self.cruby_locals(source)
+ verbose = $VERBOSE
+ $VERBOSE = nil
+
+ begin
+ locals = []
+ stack = [ISeq.new(RubyVM::InstructionSequence.compile(source).to_a)]
+
+ while (iseq = stack.pop)
+ if iseq.type != :once
+ names = iseq.local_table
+
+ # CRuby will push on a special local variable when there are keyword
+ # arguments. We get rid of that here.
+ names = names.grep_v(Integer)
+
+ # TODO: We don't support numbered local variables yet, so we get rid
+ # of those here.
+ names = names.grep_v(/^_\d$/)
+
+ # Now push them onto the list of locals.
+ locals << names
+ end
+
+ iseq.each_child { |child| stack << child }
+ end
+
+ locals
+ ensure
+ $VERBOSE = verbose
+ end
+ end
+
+ # For the given source, parses with YARP and returns a list of all of the
+ # sets of local variables that were encountered.
+ def self.yarp_locals(source)
+ locals = []
+ stack = [YARP.parse(source).value]
+
+ while (node = stack.pop)
+ case node
+ when BlockNode, DefNode, LambdaNode
+ names = node.locals
+
+ params = node.parameters
+ params = params&.parameters unless node.is_a?(DefNode)
+
+ # YARP places parameters in the same order that they appear in the
+ # source. CRuby places them in the order that they need to appear
+ # according to their own internal calling convention. We mimic that
+ # order here so that we can compare properly.
+ if params
+ sorted = [
+ *params.requireds.grep(RequiredParameterNode).map(&:constant_id),
+ *params.optionals.map(&:constant_id),
+ *((params.rest.name ? params.rest.name.to_sym : :*) if params.rest && params.rest.operator != ","),
+ *params.posts.grep(RequiredParameterNode).map(&:constant_id),
+ *params.keywords.reject(&:value).map { |param| param.name.chomp(":").to_sym },
+ *params.keywords.select(&:value).map { |param| param.name.chomp(":").to_sym }
+ ]
+
+ # TODO: When we get a ... parameter, we should be pushing * and &
+ # onto the local list. We don't do that yet, so we need to add them
+ # in here.
+ if params.keyword_rest.is_a?(ForwardingParameterNode)
+ sorted.push(:*, :&, :"...")
+ end
+
+ # Recurse down the parameter tree to find any destructured
+ # parameters and add them after the other parameters.
+ param_stack = params.requireds.concat(params.posts).grep(RequiredDestructuredParameterNode).reverse
+ while (param = param_stack.pop)
+ case param
+ when RequiredDestructuredParameterNode
+ param_stack.concat(param.parameters.reverse)
+ when RequiredParameterNode
+ sorted << param.constant_id
+ when SplatNode
+ sorted << param.expression.constant_id if param.expression
+ end
+ end
+
+ names = sorted.concat(names - sorted)
+ end
+
+ locals << names
+ when ClassNode, ModuleNode, ProgramNode, SingletonClassNode
+ locals << node.locals
+ when ForNode
+ locals << []
+ when PostExecutionNode
+ locals.push([], [])
+ end
+
+ stack.concat(node.child_nodes.compact)
+ end
+
+ locals
+ end
+
def self.newlines(source)
YARP.parse(source).source.offsets
end
def self.parse_serialize_file(filepath)
@@ -325,6 +486,10 @@
require_relative "yarp/node"
require_relative "yarp/ripper_compat"
require_relative "yarp/serialize"
require_relative "yarp/pack"
-require "yarp/yarp"
+if RUBY_ENGINE == "ruby" and !ENV["YARP_FFI_BACKEND"]
+ require "yarp/yarp"
+else
+ require "yarp/ffi"
+end