From d186eb36a4abbbefa026ea5630a1b59bb668ef0f Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 17 Apr 2024 11:28:52 -0400 Subject: [PATCH] [ruby/prism] Add a reflection API for determining the fields of a node https://github.com/ruby/prism/commit/f3f9950a74 --- lib/prism.rb | 17 +-- lib/prism/ffi.rb | 10 ++ lib/prism/prism.gemspec | 3 + prism/extension.c | 30 +++- prism/templates/lib/prism/node.rb.erb | 16 +++ prism/templates/lib/prism/reflection.rb.erb | 151 ++++++++++++++++++++ prism/templates/template.rb | 1 + test/prism/reflection_test.rb | 22 +++ 8 files changed, 232 insertions(+), 18 deletions(-) create mode 100644 prism/templates/lib/prism/reflection.rb.erb create mode 100644 test/prism/reflection_test.rb diff --git a/lib/prism.rb b/lib/prism.rb index 23a72fa49a..c512cb4015 100644 --- a/lib/prism.rb +++ b/lib/prism.rb @@ -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" diff --git a/lib/prism/ffi.rb b/lib/prism/ffi.rb index 0a064a5c94..1fd053f902 100644 --- a/lib/prism/ffi.rb +++ b/lib/prism/ffi.rb @@ -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: diff --git a/lib/prism/prism.gemspec b/lib/prism/prism.gemspec index f4be1b36bb..c0f3db5594 100644 --- a/lib/prism/prism.gemspec +++ b/lib/prism/prism.gemspec @@ -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", diff --git a/prism/extension.c b/prism/extension.c index 27e799a8da..807c8f69dc 100644 --- a/prism/extension.c +++ b/prism/extension.c @@ -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); diff --git a/prism/templates/lib/prism/node.rb.erb b/prism/templates/lib/prism/node.rb.erb index 12a984e5a2..6b5a285315 100644 --- a/prism/templates/lib/prism/node.rb.erb +++ b/prism/templates/lib/prism/node.rb.erb @@ -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| -%> diff --git a/prism/templates/lib/prism/reflection.rb.erb b/prism/templates/lib/prism/reflection.rb.erb new file mode 100644 index 0000000000..13d1da33e8 --- /dev/null +++ b/prism/templates/lib/prism/reflection.rb.erb @@ -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 diff --git a/prism/templates/template.rb b/prism/templates/template.rb index 31257ef1a1..d0ce6c6643 100755 --- a/prism/templates/template.rb +++ b/prism/templates/template.rb @@ -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", diff --git a/test/prism/reflection_test.rb b/test/prism/reflection_test.rb new file mode 100644 index 0000000000..869b68b1f8 --- /dev/null +++ b/test/prism/reflection_test.rb @@ -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