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.rb260
1 files changed, 260 insertions, 0 deletions
diff --git a/lib/prism/node_ext.rb b/lib/prism/node_ext.rb
new file mode 100644
index 0000000000..8674544065
--- /dev/null
+++ b/lib/prism/node_ext.rb
@@ -0,0 +1,260 @@
+# frozen_string_literal: true
+
+# Here we are reopening the prism module to provide methods on nodes that aren't
+# templated and are meant as convenience methods.
+module Prism
+ module RegularExpressionOptions # :nodoc:
+ # Returns a numeric value that represents the flags that were used to create
+ # the regular expression.
+ 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
+ end
+ end
+
+ class InterpolatedMatchLastLineNode < Node
+ include RegularExpressionOptions
+ end
+
+ class InterpolatedRegularExpressionNode < Node
+ include RegularExpressionOptions
+ end
+
+ class MatchLastLineNode < Node
+ include RegularExpressionOptions
+ end
+
+ class RegularExpressionNode < Node
+ include RegularExpressionOptions
+ end
+
+ private_constant :RegularExpressionOptions
+
+ module HeredocQuery # :nodoc:
+ # Returns true if this node was represented as a heredoc in the source code.
+ def heredoc?
+ opening&.start_with?("<<")
+ end
+ end
+
+ class InterpolatedStringNode < Node
+ include HeredocQuery
+ end
+
+ class InterpolatedXStringNode < Node
+ include HeredocQuery
+ end
+
+ class StringNode < Node
+ include HeredocQuery
+
+ # Occasionally it's helpful to treat a string as if it were interpolated so
+ # that there's a consistent interface for working with strings.
+ def to_interpolated
+ InterpolatedStringNode.new(
+ source,
+ frozen? ? InterpolatedStringNodeFlags::FROZEN : 0,
+ opening_loc,
+ [copy(opening_loc: nil, closing_loc: nil, location: content_loc)],
+ closing_loc,
+ location
+ )
+ end
+ end
+
+ class XStringNode < Node
+ include HeredocQuery
+
+ # Occasionally it's helpful to treat a string as if it were interpolated so
+ # that there's a consistent interface for working with strings.
+ def to_interpolated
+ InterpolatedXStringNode.new(
+ source,
+ opening_loc,
+ [StringNode.new(source, 0, nil, content_loc, nil, unescaped, content_loc)],
+ closing_loc,
+ location
+ )
+ end
+ end
+
+ private_constant :HeredocQuery
+
+ class ImaginaryNode < Node
+ # Returns the value of the node as a Ruby Complex.
+ def value
+ Complex(0, numeric.value)
+ end
+ end
+
+ class RationalNode < Node
+ # Returns the value of the node as a Ruby Rational.
+ def value
+ Rational(numeric.is_a?(IntegerNode) ? numeric.value : slice.chomp("r"))
+ end
+ end
+
+ class ConstantReadNode < Node
+ # Returns the list of parts for the full name of this constant.
+ # For example: [:Foo]
+ def full_name_parts
+ [name]
+ end
+
+ # Returns the full name of this constant. For example: "Foo"
+ def full_name
+ name.to_s
+ end
+ end
+
+ class ConstantWriteNode < Node
+ # Returns the list of parts for the full name of this constant.
+ # For example: [:Foo]
+ def full_name_parts
+ [name]
+ end
+
+ # Returns the full name of this constant. For example: "Foo"
+ def full_name
+ name.to_s
+ end
+ end
+
+ class ConstantPathNode < Node
+ # 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 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 = [] #: Array[Symbol]
+ current = self #: node?
+
+ while current.is_a?(ConstantPathNode)
+ 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
+
+ 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"
+ 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]
+ def full_name_parts
+ 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)
+ end
+
+ # Returns the full name of this constant path. For example: "Foo::Bar"
+ 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]
+ def full_name_parts
+ [name]
+ end
+
+ # Returns the full name of this constant. For example: "Foo"
+ def full_name
+ name.to_s
+ end
+ end
+
+ class ParametersNode < Node
+ # Mirrors the Method#parameters method.
+ 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 && rest.is_a?(RestParameterNode)
+ names << [:rest, rest.name || :*]
+ end
+
+ posts.each do |param|
+ 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
+ # 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
+ when ForwardingParameterNode
+ names.concat([[:rest, :*], [:keyrest, :**], [:block, :&]])
+ when KeywordRestParameterNode
+ names << [:keyrest, keyword_rest.name || :**]
+ when NoKeywordsParameterNode
+ names << [:nokey]
+ end
+
+ names << [:block, block.name || :&] if block
+ names
+ end
+ end
+end