[ruby/prism] Add a reflection API for determining the fields of a node

https://github.com/ruby/prism/commit/f3f9950a74
This commit is contained in:
Kevin Newton 2024-04-17 11:28:52 -04:00
parent ee6e591b6a
commit d186eb36a4
8 changed files with 232 additions and 18 deletions

View File

@ -24,6 +24,7 @@ module Prism
autoload :NodeInspector, "prism/node_inspector"
autoload :Pack, "prism/pack"
autoload :Pattern, "prism/pattern"
autoload :Reflection, "prism/reflection"
autoload :Serialize, "prism/serialize"
autoload :Translation, "prism/translation"
autoload :Visitor, "prism/visitor"
@ -64,22 +65,6 @@ module Prism
def self.load(source, serialized)
Serialize.load(source, serialized)
end
# :call-seq:
# Prism::parse_failure?(source, **options) -> bool
#
# Returns true if the source parses with errors.
def self.parse_failure?(source, **options)
!parse_success?(source, **options)
end
# :call-seq:
# Prism::parse_file_failure?(filepath, **options) -> bool
#
# Returns true if the file at filepath parses with errors.
def self.parse_file_failure?(filepath, **options)
!parse_file_success?(filepath, **options)
end
end
require_relative "prism/node"

View File

@ -286,12 +286,22 @@ module Prism
LibRubyParser::PrismString.with_string(code) { |string| parse_file_success_common(string, options) }
end
# Mirror the Prism.parse_failure? API by using the serialization API.
def parse_failure?(code, **options)
!parse_success?(code, **options)
end
# Mirror the Prism.parse_file_success? API by using the serialization API.
def parse_file_success?(filepath, **options)
options[:filepath] = filepath
LibRubyParser::PrismString.with_file(filepath) { |string| parse_file_success_common(string, options) }
end
# Mirror the Prism.parse_file_failure? API by using the serialization API.
def parse_file_failure?(filepath, **options)
!parse_file_success?(filepath, **options)
end
private
def dump_common(string, options) # :nodoc:

View File

@ -87,6 +87,7 @@ Gem::Specification.new do |spec|
"lib/prism/parse_result/newlines.rb",
"lib/prism/pattern.rb",
"lib/prism/polyfill/string.rb",
"lib/prism/reflection.rb",
"lib/prism/serialize.rb",
"lib/prism/translation.rb",
"lib/prism/translation/parser.rb",
@ -134,6 +135,7 @@ Gem::Specification.new do |spec|
"sig/prism/pack.rbs",
"sig/prism/parse_result.rbs",
"sig/prism/pattern.rbs",
"sig/prism/reflection.rbs",
"sig/prism/serialize.rbs",
"sig/prism/visitor.rbs",
"rbi/prism.rbi",
@ -143,6 +145,7 @@ Gem::Specification.new do |spec|
"rbi/prism/node_ext.rbi",
"rbi/prism/node.rbi",
"rbi/prism/parse_result.rbi",
"rbi/prism/reflection.rbi",
"rbi/prism/translation/parser/compiler.rbi",
"rbi/prism/translation/ripper.rbi",
"rbi/prism/translation/ripper/ripper_compiler.rbi",

View File

@ -969,7 +969,7 @@ parse_input_success_p(pm_string_t *input, const pm_options_t *options) {
/**
* call-seq:
* Prism::parse_success?(source, **options) -> Array
* Prism::parse_success?(source, **options) -> bool
*
* Parse the given string and return true if it parses without errors. For
* supported options, see Prism::parse.
@ -989,7 +989,19 @@ parse_success_p(int argc, VALUE *argv, VALUE self) {
/**
* call-seq:
* Prism::parse_file_success?(filepath, **options) -> Array
* Prism::parse_failure?(source, **options) -> bool
*
* Parse the given string and return true if it parses with errors. For
* supported options, see Prism::parse.
*/
static VALUE
parse_failure_p(int argc, VALUE *argv, VALUE self) {
return RTEST(parse_success_p(argc, argv, self)) ? Qfalse : Qtrue;
}
/**
* call-seq:
* Prism::parse_file_success?(filepath, **options) -> bool
*
* Parse the given file and return true if it parses without errors. For
* supported options, see Prism::parse.
@ -1008,6 +1020,18 @@ parse_file_success_p(int argc, VALUE *argv, VALUE self) {
return result;
}
/**
* call-seq:
* Prism::parse_file_failure?(filepath, **options) -> bool
*
* Parse the given file and return true if it parses with errors. For
* supported options, see Prism::parse.
*/
static VALUE
parse_file_failure_p(int argc, VALUE *argv, VALUE self) {
return RTEST(parse_file_success_p(argc, argv, self)) ? Qfalse : Qtrue;
}
/******************************************************************************/
/* Utility functions exposed to make testing easier */
/******************************************************************************/
@ -1366,7 +1390,9 @@ Init_prism(void) {
rb_define_singleton_method(rb_cPrism, "parse_lex", parse_lex, -1);
rb_define_singleton_method(rb_cPrism, "parse_lex_file", parse_lex_file, -1);
rb_define_singleton_method(rb_cPrism, "parse_success?", parse_success_p, -1);
rb_define_singleton_method(rb_cPrism, "parse_failure?", parse_failure_p, -1);
rb_define_singleton_method(rb_cPrism, "parse_file_success?", parse_file_success_p, -1);
rb_define_singleton_method(rb_cPrism, "parse_file_failure?", parse_file_failure_p, -1);
#ifndef PRISM_EXCLUDE_SERIALIZATION
rb_define_singleton_method(rb_cPrism, "dump", dump, -1);

View File

@ -60,6 +60,17 @@ module Prism
DotVisitor.new.tap { |visitor| accept(visitor) }.to_dot
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
@ -102,6 +113,11 @@ module Prism
def inspect(inspector = NodeInspector.new)
raise NoMethodError, "undefined method `inspect' for #{inspect}"
end
# Returns the type of the node as a symbol.
def self.type
raise NoMethodError, "undefined method `type' for #{inspect}"
end
end
<%- nodes.each do |node| -%>

View File

@ -0,0 +1,151 @@
module Prism
# The Reflection module provides the ability to reflect on the structure of
# the syntax tree itself, as opposed to looking at a single syntax tree. This
# is useful in metaprogramming contexts.
module Reflection
# A field represents a single piece of data on a node. It is the base class
# for all other field types.
class Field
# The name of the field.
attr_reader :name
# Initializes the field with the given name.
def initialize(name)
@name = name
end
end
# A node field represents a single child node in the syntax tree. It
# resolves to a Prism::Node in Ruby.
class NodeField < Field
end
# An optional node field represents a single child node in the syntax tree
# that may or may not be present. It resolves to either a Prism::Node or nil
# in Ruby.
class OptionalNodeField < Field
end
# A node list field represents a list of child nodes in the syntax tree. It
# resolves to an array of Prism::Node instances in Ruby.
class NodeListField < Field
end
# A constant field represents a constant value on a node. Effectively, it
# represents an identifier found within the source. It resolves to a symbol
# in Ruby.
class ConstantField < Field
end
# An optional constant field represents a constant value on a node that may
# or may not be present. It resolves to either a symbol or nil in Ruby.
class OptionalConstantField < Field
end
# A constant list field represents a list of constant values on a node. It
# resolves to an array of symbols in Ruby.
class ConstantListField < Field
end
# A string field represents a string value on a node. It almost always
# represents the unescaped value of a string-like literal. It resolves to a
# string in Ruby.
class StringField < Field
end
# A location field represents the location of some part of the node in the
# source code. For example, the location of a keyword or an operator. It
# resolves to a Prism::Location in Ruby.
class LocationField < Field
end
# An optional location field represents the location of some part of the
# node in the source code that may or may not be present. It resolves to
# either a Prism::Location or nil in Ruby.
class OptionalLocationField < Field
end
# A uint8 field represents an unsigned 8-bit integer value on a node. It
# resolves to an Integer in Ruby.
class UInt8Field < Field
end
# A uint32 field represents an unsigned 32-bit integer value on a node. It
# resolves to an Integer in Ruby.
class UInt32Field < Field
end
# A flags field represents a bitset of flags on a node. It resolves to an
# integer in Ruby. Note that the flags cannot be accessed directly on the
# node because the integer is kept private. Instead, the various flags in
# the bitset should be accessed through their query methods.
class FlagsField < Field
# The names of the flags in the bitset.
attr_reader :flags
# Initializes the flags field with the given name and flags.
def initialize(name, flags)
super(name)
@flags = flags
end
end
# An integer field represents an arbitrarily-sized integer value. It is used
# exclusively to represent the value of an integer literal. It resolves to
# an Integer in Ruby.
class IntegerField < Field
end
# A double field represents a double-precision floating point value. It is
# used exclusively to represent the value of a floating point literal. It
# resolves to a Float in Ruby.
class DoubleField < Field
end
# Returns the fields for the given node.
def self.fields_for(node)
case node.type
<%- nodes.each do |node| -%>
when :<%= node.human %>
[<%= node.fields.map { |field|
case field
when Prism::Template::NodeField
"NodeField.new(:#{field.name})"
when Prism::Template::OptionalNodeField
"OptionalNodeField.new(:#{field.name})"
when Prism::Template::NodeListField
"NodeListField.new(:#{field.name})"
when Prism::Template::ConstantField
"ConstantField.new(:#{field.name})"
when Prism::Template::OptionalConstantField
"OptionalConstantField.new(:#{field.name})"
when Prism::Template::ConstantListField
"ConstantListField.new(:#{field.name})"
when Prism::Template::StringField
"StringField.new(:#{field.name})"
when Prism::Template::LocationField
"LocationField.new(:#{field.name})"
when Prism::Template::OptionalLocationField
"OptionalLocationField.new(:#{field.name})"
when Prism::Template::UInt8Field
"UInt8Field.new(:#{field.name})"
when Prism::Template::UInt32Field
"UInt32Field.new(:#{field.name})"
when Prism::Template::FlagsField
found = flags.find { |flag| flag.name == field.kind }.tap { |found| raise "Expected to find #{field.kind}" unless found }
"FlagsField.new(:#{field.name}, [#{found.values.map { |value| ":#{value.name.downcase}?" }.join(", ")}])"
when Prism::Template::IntegerField
"IntegerField.new(:#{field.name})"
when Prism::Template::DoubleField
"DoubleField.new(:#{field.name})"
else
raise field.class.name
end
}.join(", ") %>]
<%- end -%>
else
raise "Unknown node type: #{node.type.inspect}"
end
end
end
end

View File

@ -631,6 +631,7 @@ module Prism
"lib/prism/dsl.rb",
"lib/prism/mutation_compiler.rb",
"lib/prism/node.rb",
"lib/prism/reflection.rb",
"lib/prism/serialize.rb",
"lib/prism/visitor.rb",
"src/diagnostic.c",

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
require_relative "test_helper"
module Prism
class ReflectionTest < TestCase
def test_fields_for
fields = Reflection.fields_for(CallNode)
methods = CallNode.instance_methods(false)
fields.each do |field|
if field.is_a?(Reflection::FlagsField)
field.flags.each do |flag|
assert_includes methods, flag
end
else
assert_includes methods, field.name
end
end
end
end
end