summaryrefslogtreecommitdiff
path: root/lib/prism/node_ext.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/prism/node_ext.rb')
-rw-r--r--lib/prism/node_ext.rb353
1 files changed, 321 insertions, 32 deletions
diff --git a/lib/prism/node_ext.rb b/lib/prism/node_ext.rb
index 264d3a5c1e..8a6624e76d 100644
--- a/lib/prism/node_ext.rb
+++ b/lib/prism/node_ext.rb
@@ -1,99 +1,388 @@
# frozen_string_literal: true
+# :markup: markdown
+#--
+# rbs_inline: enabled
+#--
# Here we are reopening the prism module to provide methods on nodes that aren't
# templated and are meant as convenience methods.
+#++
module Prism
- class FloatNode < Node
- # Returns the value of the node as a Ruby Float.
- def value
- Float(slice)
+ class Node
+ #: (*String replacements) -> void
+ def deprecated(*replacements) # :nodoc:
+ location = caller_locations(1, 1)&.[](0)&.label
+ suggest = replacements.map { |replacement| "#{self.class}##{replacement}" }
+
+ warn(<<~MSG, uplevel: 1, category: :deprecated)
+ [deprecation]: #{self.class}##{location} is deprecated and will be \
+ removed in the next major version. Use #{suggest.join("/")} instead.
+ #{(caller(1, 3) || []).join("\n")}
+ MSG
end
end
- class ImaginaryNode < Node
- # Returns the value of the node as a Ruby Complex.
- def value
- Complex(0, numeric.value)
+ module RegularExpressionOptions # :nodoc:
+ # Returns a numeric value that represents the flags that were used to create
+ # the regular expression.
+ #--
+ #: (Integer flags) -> Integer
+ def self.options(flags)
+ o = 0
+ o |= Regexp::IGNORECASE if flags.anybits?(RegularExpressionFlags::IGNORE_CASE)
+ o |= Regexp::EXTENDED if flags.anybits?(RegularExpressionFlags::EXTENDED)
+ o |= Regexp::MULTILINE if flags.anybits?(RegularExpressionFlags::MULTI_LINE)
+ 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 IntegerNode < Node
- # Returns the value of the node as a Ruby Integer.
- def value
- Integer(slice)
+ class InterpolatedMatchLastLineNode < Node
+ # Returns a numeric value that represents the flags that were used to create
+ # the regular expression.
+ #--
+ #: () -> Integer
+ def options
+ RegularExpressionOptions.options(flags)
end
end
class InterpolatedRegularExpressionNode < Node
# Returns a numeric value that represents the flags that were used to create
# the regular expression.
+ #--
+ #: () -> Integer
def options
- o = flags & (RegularExpressionFlags::IGNORE_CASE | RegularExpressionFlags::EXTENDED | RegularExpressionFlags::MULTI_LINE)
- 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
+ RegularExpressionOptions.options(flags)
end
end
- class RationalNode < Node
- # Returns the value of the node as a Ruby Rational.
- def value
- Rational(slice.chomp("r"))
+ class MatchLastLineNode < Node
+ # Returns a numeric value that represents the flags that were used to create
+ # the regular expression.
+ #--
+ #: () -> Integer
+ def options
+ RegularExpressionOptions.options(flags)
end
end
class RegularExpressionNode < Node
# Returns a numeric value that represents the flags that were used to create
# the regular expression.
+ #--
+ #: () -> Integer
def options
- o = flags & (RegularExpressionFlags::IGNORE_CASE | RegularExpressionFlags::EXTENDED | RegularExpressionFlags::MULTI_LINE)
- 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
+ RegularExpressionOptions.options(flags)
+ end
+ end
+
+ private_constant :RegularExpressionOptions
+
+ module HeredocQuery # :nodoc:
+ # Returns true if this node was represented as a heredoc in the source code.
+ #--
+ #: (String? opening) -> bool?
+ def self.heredoc?(opening)
+ # @type self: InterpolatedStringNode | InterpolatedXStringNode | StringNode | XStringNode
+ opening&.start_with?("<<")
+ end
+ end
+
+ class InterpolatedStringNode < Node
+ # Returns true if this node was represented as a heredoc in the source code.
+ #--
+ #: () -> bool?
+ def heredoc?
+ HeredocQuery.heredoc?(opening)
+ end
+ end
+
+ class InterpolatedXStringNode < Node
+ # Returns true if this node was represented as a heredoc in the source code.
+ #--
+ #: () -> bool?
+ def heredoc?
+ HeredocQuery.heredoc?(opening)
+ end
+ end
+
+ class StringNode < Node
+ # Returns true if this node was represented as a heredoc in the source code.
+ #--
+ #: () -> bool?
+ def heredoc?
+ HeredocQuery.heredoc?(opening)
+ end
+
+ # Occasionally it's helpful to treat a string as if it were interpolated so
+ # that there's a consistent interface for working with strings.
+ #--
+ #: () -> InterpolatedStringNode
+ def to_interpolated
+ InterpolatedStringNode.new(
+ source,
+ -1,
+ location,
+ frozen? ? InterpolatedStringNodeFlags::FROZEN : 0,
+ opening_loc,
+ [copy(location: content_loc, opening_loc: nil, closing_loc: nil)],
+ closing_loc
+ )
+ end
+ end
+
+ class XStringNode < Node
+ # Returns true if this node was represented as a heredoc in the source code.
+ #--
+ #: () -> bool?
+ def heredoc?
+ HeredocQuery.heredoc?(opening)
+ end
+
+ # Occasionally it's helpful to treat a string as if it were interpolated so
+ # that there's a consistent interface for working with strings.
+ #--
+ #: () -> InterpolatedXStringNode
+ def to_interpolated
+ InterpolatedXStringNode.new(
+ source,
+ -1,
+ location,
+ flags,
+ opening_loc,
+ [StringNode.new(source, node_id, content_loc, 0, nil, content_loc, nil, unescaped)],
+ closing_loc
+ )
+ end
+ end
+
+ private_constant :HeredocQuery
+
+ class ImaginaryNode < Node
+ # Returns the value of the node as a Ruby Complex.
+ #--
+ #: () -> Complex
+ def value
+ Complex(0, numeric.value)
+ end
+ end
+
+ class RationalNode < Node
+ # Returns the value of the node as a Ruby Rational.
+ #--
+ #: () -> Rational
+ def value
+ Rational(numerator, denominator)
end
end
class ConstantReadNode < Node
- # Returns the list of parts for the full name of this constant. For example: [:Foo]
+ # Returns the list of parts for the full name of this constant.
+ # For example: [:Foo]
+ #--
+ #: () -> Array[Symbol]
def full_name_parts
[name]
end
# Returns the full name of this constant. For example: "Foo"
+ #--
+ #: () -> String
def full_name
- name.name
+ name.to_s
+ end
+ end
+
+ class ConstantWriteNode < Node
+ # Returns the list of parts for the full name of this constant.
+ # For example: [:Foo]
+ #--
+ #: () -> Array[Symbol]
+ def full_name_parts
+ [name]
+ end
+
+ # Returns the full name of this constant. For example: "Foo"
+ #--
+ #: () -> String
+ def full_name
+ name.to_s
end
end
class ConstantPathNode < Node
- # Returns the list of parts for the full name of this constant path. For example: [:Foo, :Bar]
+ # An error class raised when dynamic parts are found while computing a
+ # constant path's full name. For example:
+ # Foo::Bar::Baz -> does not raise because all parts of the constant path are
+ # simple constants
+ # var::Bar::Baz -> raises because the first part of the constant path is a
+ # local variable
+ class DynamicPartsInConstantPathError < StandardError; end
+
+ # An error class raised when error recovery 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 ErrorRecoveryNodesInConstantPathError < StandardError; end
+
+ # Returns the list of parts for the full name of this constant path.
+ # For example: [:Foo, :Bar]
+ #--
+ #: () -> Array[Symbol]
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)
+ name = current.name
+ if name.nil?
+ raise ErrorRecoveryNodesInConstantPathError, "Constant path contains error recovery nodes. Cannot compute full name"
+ end
+
+ parts.unshift(name)
current = current.parent
end
+ if !current.is_a?(ConstantReadNode) && !current.nil?
+ raise DynamicPartsInConstantPathError, "Constant path contains dynamic parts. Cannot compute full name"
+ end
+
parts.unshift(current&.name || :"")
end
# Returns the full name of this constant path. For example: "Foo::Bar"
+ #--
+ #: () -> String
def full_name
full_name_parts.join("::")
end
end
class ConstantPathTargetNode < Node
- # Returns the list of parts for the full name of this constant path. For example: [:Foo, :Bar]
+ # Returns the list of parts for the full name of this constant path.
+ # For example: [:Foo, :Bar]
+ #--
+ #: () -> Array[Symbol]
def full_name_parts
- (parent&.full_name_parts || [:""]).push(child.name)
+ parts =
+ case (parent = self.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 (name = self.name).nil?
+ raise ConstantPathNode::ErrorRecoveryNodesInConstantPathError, "Constant target path contains error recovery nodes. Cannot compute full name"
+ end
+
+ parts.push(name)
end
# Returns the full name of this constant path. For example: "Foo::Bar"
+ #--
+ #: () -> String
def full_name
full_name_parts.join("::")
end
end
+
+ class ConstantTargetNode < Node
+ # Returns the list of parts for the full name of this constant.
+ # For example: [:Foo]
+ #--
+ #: () -> Array[Symbol]
+ def full_name_parts
+ [name]
+ end
+
+ # Returns the full name of this constant. For example: "Foo"
+ #--
+ #: () -> String
+ def full_name
+ name.to_s
+ end
+ end
+
+ class ParametersNode < Node
+ # Mirrors the Method#parameters method.
+ #--
+ #: () -> Array[[Symbol, Symbol] | [Symbol]]
+ def signature
+ names = [] #: Array[[Symbol, Symbol] | [Symbol]]
+
+ requireds.each do |param|
+ names << (param.is_a?(MultiTargetNode) ? [:req] : [:req, param.name])
+ end
+
+ optionals.each { |param| names << [:opt, param.name] }
+
+ if (rest = self.rest).is_a?(RestParameterNode)
+ names << [:rest, rest.name || :*]
+ end
+
+ posts.each do |param|
+ case param
+ when MultiTargetNode
+ names << [:req]
+ when ErrorRecoveryNode
+ raise "Invalid syntax"
+ else
+ names << [:req, param.name]
+ end
+ end
+
+ # Regardless of the order in which the keywords were defined, the required
+ # keywords always come first followed by the optional keywords.
+ keyopt = [] #: Array[OptionalKeywordParameterNode]
+ keywords.each do |param|
+ if param.is_a?(OptionalKeywordParameterNode)
+ keyopt << param
+ else
+ names << [:keyreq, param.name]
+ end
+ end
+
+ keyopt.each { |param| names << [:key, param.name] }
+
+ case (keyword_rest = self.keyword_rest)
+ when ForwardingParameterNode
+ names.concat([[:rest, :*], [:keyrest, :**], [:block, :&]])
+ when KeywordRestParameterNode
+ names << [:keyrest, keyword_rest.name || :**]
+ when NoKeywordsParameterNode
+ names << [:nokey]
+ end
+
+ case (block = self.block)
+ when BlockParameterNode
+ names << [:block, block.name || :&]
+ when NoBlockParameterNode
+ names << [:noblock]
+ end
+
+ names
+ end
+ end
+
+ class CallNode < Node
+ # When a call node has the attribute_write flag set, it means that the call
+ # is using the attribute write syntax. This is either a method call to []=
+ # or a method call to a method that ends with =. Either way, the = sign is
+ # present in the source.
+ #
+ # Prism returns the message_loc _without_ the = sign attached, because there
+ # can be any amount of space between the message and the = sign. However,
+ # sometimes you want the location of the full message including the inner
+ # space and the = sign. This method provides that.
+ #--
+ #: () -> Location?
+ def full_message_loc
+ attribute_write? ? message_loc&.adjoin("=") : message_loc
+ end
+ end
end