lib/yarp.rb in yarp-0.12.0 vs lib/yarp.rb in yarp-0.13.0

- old
+ new

@@ -1,616 +1,4 @@ # frozen_string_literal: true -module YARP - # This represents a source of Ruby code that has been parsed. It is used in - # conjunction with locations to allow them to resolve line numbers and source - # ranges. - class Source - attr_reader :source, :offsets - - def initialize(source, offsets = compute_offsets(source)) - @source = source - @offsets = offsets - end - - def slice(offset, length) - source.byteslice(offset, length) - end - - def line(value) - offsets.bsearch_index { |offset| offset > value } || offsets.length - end - - def line_offset(value) - offsets[line(value) - 1] - 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 - # offset and length. - protected attr_reader :source - - # The byte offset from the beginning of the source where this location - # starts. - attr_reader :start_offset - - # The length of this location in bytes. - attr_reader :length - - # The list of comments attached to this location - attr_reader :comments - - def initialize(source, start_offset, length) - @source = source - @start_offset = start_offset - @length = length - @comments = [] - end - - # Create a new location object with the given options. - def copy(**options) - Location.new( - options.fetch(:source) { source }, - options.fetch(:start_offset) { start_offset }, - options.fetch(:length) { length } - ) - end - - # Returns a string representation of this location. - def inspect - "#<YARP::Location @start_offset=#{@start_offset} @length=#{@length}>" - end - - # The source code that this location represents. - def slice - source.slice(start_offset, length) - end - - # The byte offset from the beginning of the source where this location ends. - def end_offset - start_offset + length - end - - # The line number where this location starts. - def start_line - source.line(start_offset) - end - - # The content of the line where this location starts before this location. - def start_line_slice - offset = source.line_offset(start_offset) - source.slice(offset, start_offset - offset) - end - - # The line number where this location ends. - def end_line - source.line(end_offset - 1) - end - - # The column number in bytes where this location starts from the start of - # the line. - def start_column - source.column(start_offset) - end - - # The column number in bytes where this location ends from the start of the - # line. - def end_column - source.column(end_offset) - end - - def deconstruct_keys(keys) - { start_offset: start_offset, end_offset: end_offset } - end - - def pretty_print(q) - q.text("(#{start_offset}...#{end_offset})") - end - - def ==(other) - other.is_a?(Location) && - other.start_offset == start_offset && - other.end_offset == end_offset - end - - # Returns a new location that stretches from this location to the given - # other location. Raises an error if this location is not before the other - # location or if they don't share the same source. - def join(other) - raise "Incompatible sources" if source != other.source - raise "Incompatible locations" if start_offset > other.start_offset - - Location.new(source, start_offset, other.end_offset - start_offset) - end - - def self.null - new(0, 0) - 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 - end - - def deconstruct_keys(keys) - { type: type, location: location } - end - - # Returns true if the comment happens on the same line as other code and false if the comment is by itself - def trailing? - type == :inline && !location.start_line_slice.strip.empty? - end - - def inspect - "#<YARP::Comment @type=#{@type.inspect} @location=#{@location.inspect}>" - end - end - - # This represents an error that was encountered during parsing. - class ParseError - attr_reader :message, :location - - def initialize(message, location) - @message = message - @location = location - end - - def deconstruct_keys(keys) - { message: message, location: location } - end - - def inspect - "#<YARP::ParseError @message=#{@message.inspect} @location=#{@location.inspect}>" - end - end - - # This represents a warning that was encountered during parsing. - class ParseWarning - attr_reader :message, :location - - def initialize(message, location) - @message = message - @location = location - end - - def deconstruct_keys(keys) - { message: message, location: location } - end - - def inspect - "#<YARP::ParseWarning @message=#{@message.inspect} @location=#{@location.inspect}>" - end - end - - # A class that knows how to walk down the tree. None of the individual visit - # methods are implemented on this visitor, so it forces the consumer to - # implement each one that they need. For a default implementation that - # continues walking the tree, see the Visitor class. - class BasicVisitor - def visit(node) - node&.accept(self) - end - - def visit_all(nodes) - nodes.map { |node| visit(node) } - end - - def visit_child_nodes(node) - visit_all(node.child_nodes) - end - end - - class Visitor < BasicVisitor - end - - # This represents the result of a call to ::parse or ::parse_file. It contains - # the AST, any comments that were encounters, and any errors that were - # encountered. - class ParseResult - attr_reader :value, :comments, :errors, :warnings, :source - - def initialize(value, comments, errors, warnings, source) - @value = value - @comments = comments - @errors = errors - @warnings = warnings - @source = source - end - - def deconstruct_keys(keys) - { value: value, comments: comments, errors: errors, warnings: warnings } - end - - def success? - errors.empty? - end - - def failure? - !success? - end - end - - # This represents a token from the Ruby source. - class Token - attr_reader :type, :value, :location - - def initialize(type, value, location) - @type = type - @value = value - @location = location - end - - def deconstruct_keys(keys) - { type: type, value: value, location: location } - end - - def pretty_print(q) - q.group do - q.text(type.to_s) - self.location.pretty_print(q) - q.text("(") - q.nest(2) do - q.breakable("") - q.pp(value) - end - q.breakable("") - q.text(")") - end - end - - def ==(other) - other.is_a?(Token) && - other.type == type && - other.value == value - end - end - - # This represents a node in the tree. - class Node - attr_reader :location - - def newline? - @newline ? true : false - end - - def set_newline_flag(newline_marked) - line = location.start_line - unless newline_marked[line] - 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.text(inspect.chomp) - end - end - - # This object is responsible for generating the output for the inspect method - # implementations of child nodes. - class NodeInspector - attr_reader :prefix, :output - - def initialize(prefix = "") - @prefix = prefix - @output = +"" - end - - # Appends a line to the output with the current prefix. - def <<(line) - output << "#{prefix}#{line}" - end - - # This generates a string that is used as the header of the inspect output - # for any given node. - def header(node) - output = +"@ #{node.class.name.split("::").last} (" - output << "location: (#{node.location.start_offset}...#{node.location.end_offset})" - output << ", newline: true" if node.newline? - output << ")\n" - output - end - - # Generates a string that represents a list of nodes. It handles properly - # using the box drawing characters to make the output look nice. - def list(prefix, nodes) - output = +"(length: #{nodes.length})\n" - last_index = nodes.length - 1 - - nodes.each_with_index do |node, index| - pointer, preadd = (index == last_index) ? ["└── ", " "] : ["├── ", "│ "] - node_prefix = "#{prefix}#{preadd}" - output << node.inspect(NodeInspector.new(node_prefix)).sub(node_prefix, "#{prefix}#{pointer}") - end - - output - end - - # Generates a string that represents a location field on a node. - def location(value) - if value - "(#{value.start_offset}...#{value.end_offset}) = #{value.slice.inspect}" - else - "∅" - end - end - - # Generates a string that represents a child node. - def child_node(node, append) - node.inspect(child_inspector(append)).delete_prefix(prefix) - end - - # Returns a new inspector that can be used to inspect a child node. - def child_inspector(append) - NodeInspector.new("#{prefix}#{append}") - end - - # Returns the output as a string. - def to_str - output - end - end - - # Load the serialized AST using the source as a reference into a tree. - def self.load(source, serialized) - Serialize.load(source, serialized) - 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) - - # For some reason, CRuby occasionally pushes this special local - # variable when there are splat arguments. We get rid of that here. - names = names.grep_v(:"#arg_rest") - - # 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(&:name), - *params.optionals.map(&:name), - *((params.rest.name || :*) if params.rest && params.rest.operator != ","), - *params.posts.grep(RequiredParameterNode).map(&:name), - *params.keywords.reject(&:value).map(&:name), - *params.keywords.select(&:value).map(&:name) - ] - - # 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.name - when SplatNode - sorted << param.expression.name 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([], []) - when InterpolatedRegularExpressionNode - locals << [] if node.once? - 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) - parse_serialize_file_metadata(filepath, [filepath.bytesize, filepath.b, 0].pack("LA*L")) - end - end - - # Marking this as private so that consumers don't see it. It makes it a little - # annoying for testing since you have to const_get it to access the methods, - # but at least this way it's clear it's not meant for consumers. - private_constant :Debug -end - -require_relative "yarp/lex_compat" -require_relative "yarp/mutation_visitor" -require_relative "yarp/desugar_visitor" -require_relative "yarp/node" -require_relative "yarp/ripper_compat" -require_relative "yarp/serialize" -require_relative "yarp/pack" -require_relative "yarp/pattern" - -require_relative "yarp/parse_result/comments" -require_relative "yarp/parse_result/newlines" - -if RUBY_ENGINE == "ruby" and !ENV["YARP_FFI_BACKEND"] - require "yarp/yarp" -else - require_relative "yarp/ffi" -end - -# Reopening the YARP module after yarp/node is required so that constant -# reflection APIs will find the constants defined in the node file before these. -# This block is meant to contain extra APIs we define on YARP nodes that aren't -# templated and are meant as convenience methods. -module YARP - class FloatNode < Node - # Returns the value of the node as a Ruby Float. - def value - Float(slice) - end - end - - class ImaginaryNode < Node - # Returns the value of the node as a Ruby Complex. - def value - Complex(0, numeric.value) - end - end - - class IntegerNode < Node - # Returns the value of the node as a Ruby Integer. - def value - Integer(slice) - end - end - - class InterpolatedRegularExpressionNode < Node - # Returns a numeric value that represents the flags that were used to create - # the regular expression. - def options - o = flags & 0b111 - o |= Regexp::FIXEDENCODING if flags.anybits?(RegularExpressionFlags::EUC_JP | RegularExpressionFlags::WINDOWS_31J | RegularExpressionFlags::UTF_8) - o |= Regexp::NOENCODING if flags.anybits?(RegularExpressionFlags::ASCII_8BIT) - o - end - end - - class RationalNode < Node - # Returns the value of the node as a Ruby Rational. - def value - Rational(slice.chomp("r")) - end - end - - class RegularExpressionNode < Node - # Returns a numeric value that represents the flags that were used to create - # the regular expression. - def options - o = flags & 0b111 - o |= Regexp::FIXEDENCODING if flags.anybits?(RegularExpressionFlags::EUC_JP | RegularExpressionFlags::WINDOWS_31J | RegularExpressionFlags::UTF_8) - o |= Regexp::NOENCODING if flags.anybits?(RegularExpressionFlags::ASCII_8BIT) - o - end - end -end +require "prism" +YARP = Prism