diff --git a/lib/prism/node_ext.rb b/lib/prism/node_ext.rb index 36a43b6327..fdd6ac200d 100644 --- a/lib/prism/node_ext.rb +++ b/lib/prism/node_ext.rb @@ -106,14 +106,23 @@ module Prism # 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 @@ -134,14 +143,19 @@ module Prism # 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) @@ -169,7 +183,7 @@ module Prism 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]) @@ -182,7 +196,14 @@ module Prism 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 diff --git a/lib/prism/parse_result.rb b/lib/prism/parse_result.rb index 1d9e700882..c55b7b6685 100644 --- a/lib/prism/parse_result.rb +++ b/lib/prism/parse_result.rb @@ -281,7 +281,8 @@ module Prism # 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 diff --git a/lib/prism/parse_result/comments.rb b/lib/prism/parse_result/comments.rb index 314261ea47..4e1b9a68e5 100644 --- a/lib/prism/parse_result/comments.rb +++ b/lib/prism/parse_result/comments.rb @@ -188,7 +188,12 @@ module Prism # 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 diff --git a/lib/prism/prism.gemspec b/lib/prism/prism.gemspec index 16ae553e32..905cd1b90b 100644 --- a/lib/prism/prism.gemspec +++ b/lib/prism/prism.gemspec @@ -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", diff --git a/lib/prism/ripper_compat.rb b/lib/prism/ripper_compat.rb new file mode 100644 index 0000000000..caab9a8549 --- /dev/null +++ b/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 diff --git a/prism/templates/lib/prism/dsl.rb.erb b/prism/templates/lib/prism/dsl.rb.erb index 7c55fb10bc..4c139ca281 100644 --- a/prism/templates/lib/prism/dsl.rb.erb +++ b/prism/templates/lib/prism/dsl.rb.erb @@ -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| -%> diff --git a/prism/templates/lib/prism/node.rb.erb b/prism/templates/lib/prism/node.rb.erb index 98048989f2..32134f8820 100644 --- a/prism/templates/lib/prism/node.rb.erb +++ b/prism/templates/lib/prism/node.rb.erb @@ -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 diff --git a/prism/templates/lib/prism/visitor.rb.erb b/prism/templates/lib/prism/visitor.rb.erb index 04156cc7a9..4b30a1815b 100644 --- a/prism/templates/lib/prism/visitor.rb.erb +++ b/prism/templates/lib/prism/visitor.rb.erb @@ -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