Skip to content

Commit

Permalink
[ruby/prism] Remove Ripper from public RBS, type-assert remaining issues
Browse files Browse the repository at this point in the history
  • Loading branch information
nixme authored and matzbot committed Feb 24, 2024
1 parent 7556fd9 commit 935d4fa
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 16 deletions.
47 changes: 34 additions & 13 deletions lib/prism/node_ext.rb
Expand Up @@ -106,14 +106,23 @@ class ConstantPathNode < Node
# local variable
class DynamicPartsInConstantPathError < StandardError; end

# An error class raised when missing nodes are found while computing a
# constant path's full name. For example:
# Foo:: -> raises because the constant path is missing the last part
class MissingNodesInConstantPathError < StandardError; end

# Returns the list of parts for the full name of this constant path.
# For example: [:Foo, :Bar]
def full_name_parts
parts = [child.name]
current = parent
parts = [] #: Array[Symbol]
current = self #: node?

while current.is_a?(ConstantPathNode)
parts.unshift(current.child.name)
child = current.child
if child.is_a?(MissingNode)
raise MissingNodesInConstantPathError, "Constant path contains missing nodes. Cannot compute full name"
end
parts.unshift(child.name)
current = current.parent
end

Expand All @@ -134,14 +143,19 @@ class ConstantPathTargetNode < Node
# Returns the list of parts for the full name of this constant path.
# For example: [:Foo, :Bar]
def full_name_parts
parts = case parent
when ConstantPathNode, ConstantReadNode
parent.full_name_parts
when nil
[:""]
else
raise ConstantPathNode::DynamicPartsInConstantPathError,
"Constant path target contains dynamic parts. Cannot compute full name"
parts =
case parent
when ConstantPathNode, ConstantReadNode
parent.full_name_parts
when nil
[:""]
else
# e.g. self::Foo, (var)::Bar = baz
raise ConstantPathNode::DynamicPartsInConstantPathError, "Constant target path contains dynamic parts. Cannot compute full name"
end

if child.is_a?(MissingNode)
raise ConstantPathNode::MissingNodesInConstantPathError, "Constant target path contains missing nodes. Cannot compute full name"
end

parts.push(child.name)
Expand Down Expand Up @@ -169,7 +183,7 @@ def full_name
class ParametersNode < Node
# Mirrors the Method#parameters method.
def signature
names = [] #: Array[[:req | :opt | :rest | :keyreq | :key | :keyrest | :block, Symbol] | [:req | :rest | :keyrest | :nokey]]
names = [] #: Array[[Symbol, Symbol] | [Symbol]]

requireds.each do |param|
names << (param.is_a?(MultiTargetNode) ? [:req] : [:req, param.name])
Expand All @@ -182,7 +196,14 @@ def signature
end

posts.each do |param|
names << (param.is_a?(MultiTargetNode) ? [:req] : [:req, param.name])
if param.is_a?(MultiTargetNode)
names << [:req]
elsif param.is_a?(NoKeywordsParameterNode)
# Invalid syntax, e.g. "def f(**nil, ...)" moves the NoKeywordsParameterNode to posts
raise "Invalid syntax"
else
names << [:req, param.name]
end
end

# Regardless of the order in which the keywords were defined, the required
Expand Down
3 changes: 2 additions & 1 deletion lib/prism/parse_result.rb
Expand Up @@ -281,7 +281,8 @@ def join(other)
# the beginning of the file. Useful for when you want a location object but
# do not care where it points.
def self.null
new(nil, 0, 0)
source = nil #: Source
new(source, 0, 0)
end
end

Expand Down
7 changes: 6 additions & 1 deletion lib/prism/parse_result/comments.rb
Expand Up @@ -188,7 +188,12 @@ def nearest_targets(node, comment)

# Attach the list of comments to their respective locations in the tree.
def attach_comments!
Comments.new(self).attach!
if ProgramNode === value
this = self #: ParseResult[ProgramNode]
Comments.new(this).attach!
else
raise
end
end
end
end
1 change: 0 additions & 1 deletion lib/prism/prism.gemspec
Expand Up @@ -122,7 +122,6 @@ Gem::Specification.new do |spec|
"src/options.c",
"src/prism.c",
"prism.gemspec",
"sig/manifest.yaml",
"sig/prism.rbs",
"sig/prism/compiler.rbs",
"sig/prism/dispatcher.rbs",
Expand Down
208 changes: 208 additions & 0 deletions lib/prism/ripper_compat.rb
@@ -0,0 +1,208 @@
# frozen_string_literal: true

require "ripper"

module Prism
# Note: This integration is not finished, and therefore still has many
# inconsistencies with Ripper. If you'd like to help out, pull requests would
# be greatly appreciated!
#
# This class is meant to provide a compatibility layer between prism and
# Ripper. It functions by parsing the entire tree first and then walking it
# and executing each of the Ripper callbacks as it goes.
#
# This class is going to necessarily be slower than the native Ripper API. It
# is meant as a stopgap until developers migrate to using prism. It is also
# meant as a test harness for the prism parser.
#
# To use this class, you treat `Prism::RipperCompat` effectively as you would
# treat the `Ripper` class.
class RipperCompat < Visitor
# This class mirrors the ::Ripper::SexpBuilder subclass of ::Ripper that
# returns the arrays of [type, *children].
class SexpBuilder < RipperCompat
private

Ripper::PARSER_EVENTS.each do |event|
define_method(:"on_#{event}") do |*args|
[event, *args]
end
end

Ripper::SCANNER_EVENTS.each do |event|
define_method(:"on_#{event}") do |value|
[:"@#{event}", value, [lineno, column]]
end
end
end

# This class mirrors the ::Ripper::SexpBuilderPP subclass of ::Ripper that
# returns the same values as ::Ripper::SexpBuilder except with a couple of
# niceties that flatten linked lists into arrays.
class SexpBuilderPP < SexpBuilder
private

def _dispatch_event_new # :nodoc:
[]
end

def _dispatch_event_push(list, item) # :nodoc:
list << item
list
end

Ripper::PARSER_EVENT_TABLE.each do |event, arity|
case event
when /_new\z/
alias_method :"on_#{event}", :_dispatch_event_new if arity == 0
when /_add\z/
alias_method :"on_#{event}", :_dispatch_event_push
end
end
end

# The source that is being parsed.
attr_reader :source

# The current line number of the parser.
attr_reader :lineno

# The current column number of the parser.
attr_reader :column

# Create a new RipperCompat object with the given source.
def initialize(source)
@source = source
@result = nil
@lineno = nil
@column = nil
end

############################################################################
# Public interface
############################################################################

# True if the parser encountered an error during parsing.
def error?
result.failure?
end

# Parse the source and return the result.
def parse
result.magic_comments.each do |magic_comment|
on_magic_comment(magic_comment.key, magic_comment.value)
end

if error?
result.errors.each do |error|
on_parse_error(error.message)
end
else
result.value.accept(self)
end
end

############################################################################
# Visitor methods
############################################################################

# Visit a CallNode node.
def visit_call_node(node)
message = node.message
if message && message.match?(/^[[:alpha:]_]/) && node.opening_loc.nil? && node.arguments && node.arguments.arguments && node.arguments.arguments.length == 1
left = visit(node.receiver)
right = visit(node.arguments.arguments.first)

bounds(node.location)
on_binary(left, node.name, right)
else
raise NotImplementedError
end
end

# Visit a FloatNode node.
def visit_float_node(node)
bounds(node.location)
on_float(node.slice)
end

# Visit a ImaginaryNode node.
def visit_imaginary_node(node)
bounds(node.location)
on_imaginary(node.slice)
end

# Visit an IntegerNode node.
def visit_integer_node(node)
bounds(node.location)
on_int(node.slice)
end

# Visit a RationalNode node.
def visit_rational_node(node)
bounds(node.location)
on_rational(node.slice)
end

# Visit a StatementsNode node.
def visit_statements_node(node)
bounds(node.location)
node.body.inject(on_stmts_new) do |stmts, stmt|
on_stmts_add(stmts, visit(stmt))
end
end

# Visit a ProgramNode node.
def visit_program_node(node)
statements = visit(node.statements)
bounds(node.location)
on_program(statements)
end

############################################################################
# Entrypoints for subclasses
############################################################################

# This is a convenience method that runs the SexpBuilder subclass parser.
def self.sexp_raw(source)
SexpBuilder.new(source).parse
end

# This is a convenience method that runs the SexpBuilderPP subclass parser.
def self.sexp(source)
SexpBuilderPP.new(source).parse
end

private

# This method is responsible for updating lineno and column information
# to reflect the current node.
#
# This method could be drastically improved with some caching on the start
# of every line, but for now it's good enough.
def bounds(location)
@lineno = location.start_line
@column = location.start_column
end

# Lazily initialize the parse result.
def result
@result ||= Prism.parse(source)
end

def _dispatch0; end # :nodoc:
def _dispatch1(_); end # :nodoc:
def _dispatch2(_, _); end # :nodoc:
def _dispatch3(_, _, _); end # :nodoc:
def _dispatch4(_, _, _, _); end # :nodoc:
def _dispatch5(_, _, _, _, _); end # :nodoc:
def _dispatch7(_, _, _, _, _, _, _); end # :nodoc:

alias_method :on_parse_error, :_dispatch1
alias_method :on_magic_comment, :_dispatch2

(Ripper::SCANNER_EVENT_TABLE.merge(Ripper::PARSER_EVENT_TABLE)).each do |event, arity|
alias_method :"on_#{event}", :"_dispatch#{arity}"
end
end
end
1 change: 1 addition & 0 deletions prism/templates/lib/prism/dsl.rb.erb
Expand Up @@ -36,6 +36,7 @@ module Prism

# Create a new Location object
def Location(source = nil, start_offset = 0, length = 0)
# @type var source: Source
Location.new(source, start_offset, length)
end
<%- nodes.each do |node| -%>
Expand Down
1 change: 1 addition & 0 deletions prism/templates/lib/prism/node.rb.erb
Expand Up @@ -56,6 +56,7 @@ module Prism

# Convert this node into a graphviz dot graph string.
def to_dot
# @type self: node
DotVisitor.new.tap { |visitor| accept(visitor) }.to_dot
end

Expand Down
3 changes: 3 additions & 0 deletions prism/templates/lib/prism/visitor.rb.erb
Expand Up @@ -7,16 +7,19 @@ module Prism
# Calls `accept` on the given node if it is not `nil`, which in turn should
# call back into this visitor by calling the appropriate `visit_*` method.
def visit(node)
# @type self: _Visitor
node&.accept(self)
end

# Visits each node in `nodes` by calling `accept` on each one.
def visit_all(nodes)
# @type self: _Visitor
nodes.each { |node| node&.accept(self) }
end

# Visits the child nodes of `node` by calling `accept` on each one.
def visit_child_nodes(node)
# @type self: _Visitor
node.compact_child_nodes.each { |node| node.accept(self) }
end
end
Expand Down

0 comments on commit 935d4fa

Please sign in to comment.