diff options
Diffstat (limited to 'prism/templates/lib/prism/node.rb.erb')
-rw-r--r-- | prism/templates/lib/prism/node.rb.erb | 414 |
1 files changed, 414 insertions, 0 deletions
diff --git a/prism/templates/lib/prism/node.rb.erb b/prism/templates/lib/prism/node.rb.erb new file mode 100644 index 0000000000..f033cdea9b --- /dev/null +++ b/prism/templates/lib/prism/node.rb.erb @@ -0,0 +1,414 @@ +module Prism + # This represents a node in the tree. It is the parent class of all of the + # various node types. + class Node + # A pointer to the source that this node was created from. + attr_reader :source + private :source + + # A unique identifier for this node. This is used in a very specific + # use case where you want to keep around a reference to a node without + # having to keep around the syntax tree in memory. This unique identifier + # will be consistent across multiple parses of the same source code. + attr_reader :node_id + + # A Location instance that represents the location of this node in the + # source. + def location + location = @location + return location if location.is_a?(Location) + @location = Location.new(source, location >> 32, location & 0xFFFFFFFF) + end + + # The start offset of the node in the source. This method is effectively a + # delegate method to the location object. + def start_offset + location = @location + location.is_a?(Location) ? location.start_offset : location >> 32 + end + + # The end offset of the node in the source. This method is effectively a + # delegate method to the location object. + def end_offset + location = @location + location.is_a?(Location) ? location.end_offset : ((location >> 32) + (location & 0xFFFFFFFF)) + end + + # Returns all of the lines of the source code associated with this node. + def source_lines + location.source_lines + end + + # An alias for source_lines, used to mimic the API from + # RubyVM::AbstractSyntaxTree to make it easier to migrate. + alias script_lines source_lines + + # Slice the location of the node from the source. + def slice + location.slice + end + + # Slice the location of the node from the source, starting at the beginning + # of the line that the location starts on, ending at the end of the line + # that the location ends on. + def slice_lines + location.slice_lines + end + + # An bitset of flags for this node. There are certain flags that are common + # for all nodes, and then some nodes have specific flags. + attr_reader :flags + protected :flags + + # Returns true if the node has the newline flag set. + def newline? + flags.anybits?(NodeFlags::NEWLINE) + end + + # Returns true if the node has the static literal flag set. + def static_literal? + flags.anybits?(NodeFlags::STATIC_LITERAL) + end + + # Similar to inspect, but respects the current level of indentation given by + # the pretty print object. + def pretty_print(q) + q.seplist(inspect.chomp.each_line, -> { q.breakable }) do |line| + q.text(line.chomp) + end + q.current_group.break + end + + # Convert this node into a graphviz dot graph string. + def to_dot + # @type self: node + DotVisitor.new.tap { |visitor| accept(visitor) }.to_dot + end + + # Returns a list of nodes that are descendants of this node that contain the + # given line and column. This is useful for locating a node that is selected + # based on the line and column of the source code. + # + # Important to note is that the column given to this method should be in + # bytes, as opposed to characters or code units. + def tunnel(line, column) + queue = [self] #: Array[Prism::node] + result = [] + + while (node = queue.shift) + result << node + + node.compact_child_nodes.each do |child_node| + child_location = child_node.location + + start_line = child_location.start_line + end_line = child_location.end_line + + if start_line == end_line + if line == start_line && column >= child_location.start_column && column < child_location.end_column + queue << child_node + break + end + elsif (line == start_line && column >= child_location.start_column) || (line == end_line && column < child_location.end_column) + queue << child_node + break + elsif line > start_line && line < end_line + queue << child_node + break + end + end + end + + result + end + + # Returns the first node that matches the given block when visited in a + # depth-first search. This is useful for finding a node that matches a + # particular condition. + # + # node.breadth_first_search { |node| node.node_id == node_id } + # + def breadth_first_search(&block) + queue = [self] #: Array[Prism::node] + + while (node = queue.shift) + return node if yield node + queue.concat(node.compact_child_nodes) + end + + nil + end + + # Returns a list of the fields that exist for this node class. Fields + # describe the structure of the node. This kind of reflection is useful for + # things like recursively visiting each node _and_ field in the tree. + def self.fields + # This method should only be called on subclasses of Node, not Node + # itself. + raise NoMethodError, "undefined method `fields' for #{inspect}" if self == Node + + Reflection.fields_for(self) + end + + # -------------------------------------------------------------------------- + # :section: Node interface + # These methods are effectively abstract methods that must be implemented by + # the various subclasses of Node. They are here to make it easier to work + # with typecheckers. + # -------------------------------------------------------------------------- + + # Accepts a visitor and calls back into the specialized visit function. + def accept(visitor) + raise NoMethodError, "undefined method `accept' for #{inspect}" + end + + # Returns an array of child nodes, including `nil`s in the place of optional + # nodes that were not present. + def child_nodes + raise NoMethodError, "undefined method `child_nodes' for #{inspect}" + end + + alias deconstruct child_nodes + + # Returns an array of child nodes, excluding any `nil`s in the place of + # optional nodes that were not present. + def compact_child_nodes + raise NoMethodError, "undefined method `compact_child_nodes' for #{inspect}" + end + + # Returns an array of child nodes and locations that could potentially have + # comments attached to them. + def comment_targets + raise NoMethodError, "undefined method `comment_targets' for #{inspect}" + end + + # Returns a string representation of the node. + def inspect + raise NoMethodError, "undefined method `inspect' for #{inspect}" + end + + # Sometimes you want to check an instance of a node against a list of + # classes to see what kind of behavior to perform. Usually this is done by + # calling `[cls1, cls2].include?(node.class)` or putting the node into a + # case statement and doing `case node; when cls1; when cls2; end`. Both of + # these approaches are relatively slow because of the constant lookups, + # method calls, and/or array allocations. + # + # Instead, you can call #type, which will return to you a symbol that you + # can use for comparison. This is faster than the other approaches because + # it uses a single integer comparison, but also because if you're on CRuby + # you can take advantage of the fact that case statements with all symbol + # keys will use a jump table. + def type + raise NoMethodError, "undefined method `type' for #{inspect}" + end + + # Similar to #type, this method returns a symbol that you can use for + # splitting on the type of the node without having to do a long === chain. + # Note that like #type, it will still be slower than using == for a single + # class, but should be faster in a case statement or an array comparison. + def self.type + raise NoMethodError, "undefined method `type' for #{inspect}" + end + end + <%- nodes.each do |node| -%> + + <%- node.each_comment_line do |line| -%> + #<%= line %> + <%- end -%> + class <%= node.name -%> < Node + # Initialize a new <%= node.name %> node. + def initialize(<%= ["source", "node_id", "location", "flags", *node.fields.map(&:name)].join(", ") %>) + @source = source + @node_id = node_id + @location = location + @flags = flags + <%- node.fields.each do |field| -%> + <%- if Prism::Template::CHECK_FIELD_KIND && field.respond_to?(:check_field_kind) -%> + raise "<%= node.name %>#<%= field.name %> was of unexpected type:\n#{<%= field.name %>.inspect}" unless <%= field.check_field_kind %> + <%- end -%> + @<%= field.name %> = <%= field.name %> + <%- end -%> + end + + # def accept: (Visitor visitor) -> void + def accept(visitor) + visitor.visit_<%= node.human %>(self) + end + + # def child_nodes: () -> Array[nil | Node] + def child_nodes + [<%= node.fields.map { |field| + case field + when Prism::Template::NodeField, Prism::Template::OptionalNodeField then field.name + when Prism::Template::NodeListField then "*#{field.name}" + end + }.compact.join(", ") %>] + end + + # def compact_child_nodes: () -> Array[Node] + def compact_child_nodes + <%- if node.fields.any? { |field| field.is_a?(Prism::Template::OptionalNodeField) } -%> + compact = [] #: Array[Prism::node] + <%- node.fields.each do |field| -%> + <%- case field -%> + <%- when Prism::Template::NodeField -%> + compact << <%= field.name %> + <%- when Prism::Template::OptionalNodeField -%> + compact << <%= field.name %> if <%= field.name %> + <%- when Prism::Template::NodeListField -%> + compact.concat(<%= field.name %>) + <%- end -%> + <%- end -%> + compact + <%- else -%> + [<%= node.fields.map { |field| + case field + when Prism::Template::NodeField then field.name + when Prism::Template::NodeListField then "*#{field.name}" + end + }.compact.join(", ") %>] + <%- end -%> + end + + # def comment_targets: () -> Array[Node | Location] + def comment_targets + [<%= node.fields.map { |field| + case field + when Prism::Template::NodeField, Prism::Template::LocationField then field.name + when Prism::Template::OptionalNodeField, Prism::Template::NodeListField, Prism::Template::OptionalLocationField then "*#{field.name}" + end + }.compact.join(", ") %>] #: Array[Prism::node | Location] + end + + # def copy: (<%= (["?node_id: Integer", "?location: Location", "?flags: Integer"] + node.fields.map { |field| "?#{field.name}: #{field.rbs_class}" }).join(", ") %>) -> <%= node.name %> + def copy(<%= (["node_id", "location", "flags"] + node.fields.map(&:name)).map { |field| "#{field}: self.#{field}" }.join(", ") %>) + <%= node.name %>.new(<%= ["source", "node_id", "location", "flags", *node.fields.map(&:name)].join(", ") %>) + end + + # def deconstruct: () -> Array[nil | Node] + alias deconstruct child_nodes + + # def deconstruct_keys: (Array[Symbol] keys) -> { <%= (["node_id: Integer", "location: Location"] + node.fields.map { |field| "#{field.name}: #{field.rbs_class}" }).join(", ") %> } + def deconstruct_keys(keys) + { <%= (["node_id: node_id", "location: location"] + node.fields.map { |field| "#{field.name}: #{field.name}" }).join(", ") %> } + end + <%- if (node_flags = node.flags) -%> + <%- node_flags.values.each do |value| -%> + + # def <%= value.name.downcase %>?: () -> bool + def <%= value.name.downcase %>? + flags.anybits?(<%= node_flags.name %>::<%= value.name %>) + end + <%- end -%> + <%- end -%> + <%- node.fields.each do |field| -%> + + <%- if field.comment.nil? -%> + # attr_reader <%= field.name %>: <%= field.rbs_class %> + <%- else -%> + <%- field.each_comment_line do |line| -%> + #<%= line %> + <%- end -%> + <%- end -%> + <%- case field -%> + <%- when Prism::Template::LocationField -%> + def <%= field.name %> + location = @<%= field.name %> + return location if location.is_a?(Location) + @<%= field.name %> = Location.new(source, location >> 32, location & 0xFFFFFFFF) + end + <%- when Prism::Template::OptionalLocationField -%> + def <%= field.name %> + location = @<%= field.name %> + case location + when nil + nil + when Location + location + else + @<%= field.name %> = Location.new(source, location >> 32, location & 0xFFFFFFFF) + end + end + <%- else -%> + attr_reader :<%= field.name %> + <%- end -%> + <%- end -%> + <%- node.fields.each do |field| -%> + <%- case field -%> + <%- when Prism::Template::LocationField -%> + <%- raise unless field.name.end_with?("_loc") -%> + <%- next if node.fields.any? { |other| other.name == field.name.delete_suffix("_loc") } -%> + + # def <%= field.name.delete_suffix("_loc") %>: () -> String + def <%= field.name.delete_suffix("_loc") %> + <%= field.name %>.slice + end + <%- when Prism::Template::OptionalLocationField -%> + <%- raise unless field.name.end_with?("_loc") -%> + <%- next if node.fields.any? { |other| other.name == field.name.delete_suffix("_loc") } -%> + + # def <%= field.name.delete_suffix("_loc") %>: () -> String? + def <%= field.name.delete_suffix("_loc") %> + <%= field.name %>&.slice + end + <%- end -%> + <%- end -%> + + # def inspect -> String + def inspect + InspectVisitor.compose(self) + end + + # Return a symbol representation of this node type. See `Node#type`. + def type + :<%= node.human %> + end + + # Return a symbol representation of this node type. See `Node::type`. + def self.type + :<%= node.human %> + end + + # Implements case-equality for the node. This is effectively == but without + # comparing the value of locations. Locations are checked only for presence. + def ===(other) + other.is_a?(<%= node.name %>)<%= " &&" if (fields = [*node.flags, *node.fields]).any? %> + <%- fields.each_with_index do |field, index| -%> + <%- if field.is_a?(Prism::Template::LocationField) || field.is_a?(Prism::Template::OptionalLocationField) -%> + (<%= field.name %>.nil? == other.<%= field.name %>.nil?)<%= " &&" if index != fields.length - 1 %> + <%- elsif field.is_a?(Prism::Template::NodeListField) || field.is_a?(Prism::Template::ConstantListField) -%> + (<%= field.name %>.length == other.<%= field.name %>.length) && + <%= field.name %>.zip(other.<%= field.name %>).all? { |left, right| left === right }<%= " &&" if index != fields.length - 1 %> + <%- elsif field.is_a?(Prism::Template::Flags) -%> + (flags === other.flags)<%= " &&" if index != fields.length - 1 %> + <%- else -%> + (<%= field.name %> === other.<%= field.name %>)<%= " &&" if index != fields.length - 1 %> + <%- end -%> + <%- end -%> + end + end + <%- end -%> + <%- flags.each do |flag| -%> + + # <%= flag.comment %> + module <%= flag.name %> + <%- flag.values.each_with_index do |value, index| -%> + # <%= value.comment %> + <%= value.name %> = 1 << <%= index + Prism::Template::COMMON_FLAGS_COUNT %> +<%= "\n" if value != flag.values.last -%> + <%- end -%> + end + <%- end -%> + + # The flags that are common to all nodes. + module NodeFlags + # A flag to indicate that the node is a candidate to emit a :line event + # through tracepoint when compiled. + NEWLINE = 1 + + # A flag to indicate that the value that the node represents is a value that + # can be determined at parse-time. + STATIC_LITERAL = 2 + end +end |