Skip to content

Commit

Permalink
Fallback to dynamically defining node type predicates
Browse files Browse the repository at this point in the history
While we have tests which enforce that we're calling
`def_node_type_predicate` for all known `Parser::Meta::NODE_TYPES`, it
is possible for a host application to use a newer version of `parser`,
which might support additional nodes, and for the application to attempt
to access those nodes in custom cops.

To preserve the previous forward compatibility, we fallback to
generating any missing methods. They won't be documented, but at least
they'll work.

The tests will enforce that if rubocop-ast bumps its Parser version, all
node type predicates are generated via `dev_node_type_predicate`.
  • Loading branch information
sambostock committed Nov 8, 2024
1 parent 3470a3c commit 37a86d1
Show file tree
Hide file tree
Showing 2 changed files with 36 additions and 4 deletions.
11 changes: 11 additions & 0 deletions lib/rubocop/ast/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,15 @@ def #{recursive_kind} # def recursive_litera

# Define a +_type?+ predicate method for the given node type.
private_class_method def self.def_node_type_predicate(name, type = name)
@node_types_with_documented_predicate_method << type

class_eval <<~RUBY, __FILE__, __LINE__ + 1
def #{name}_type? # def block_type?
@type == :#{type} # @type == :block
end # end
RUBY
end
@node_types_with_documented_predicate_method = []

# @see https://www.rubydoc.info/gems/ast/AST/Node:initialize
def initialize(type, children = EMPTY_CHILDREN, properties = EMPTY_PROPERTIES)
Expand Down Expand Up @@ -317,6 +320,14 @@ def send_type?
# separately to make this check as fast as possible.
false
end
@node_types_with_documented_predicate_method << :send

# Ensure forward compatibility with new node types, by defining methods for unknown node types too.
# Note these won't get auto-generated documentation, which is why we prefer defining them above.
(Parser::Meta::NODE_TYPES - @node_types_with_documented_predicate_method).each do |node_type|
method_name = :"#{node_type.to_s.gsub(/\W/, '')}_type?"
define_method(method_name) { false }
end

# @!endgroup

Expand Down
29 changes: 25 additions & 4 deletions spec/rubocop/ast/node_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1110,13 +1110,34 @@ class << expr
end
end

describe '*_type? methods on Node' do
Parser::Meta::NODE_TYPES.each do |node_type|
method_name = "#{node_type.to_s.gsub(/\W/, '')}_type?"
Parser::Meta::NODE_TYPES.each do |node_type|
node_name = node_type.to_s.gsub(/\W/, '')
method_name = :"#{node_name}_type?"

it "is not of #{method_name}" do
describe "##{method_name}" do
it 'is false' do
expect(described_class.allocate.public_send(method_name)).to be(false)
end

it 'is documented' do
is_documented = described_class
.instance_variable_get(:@node_types_with_documented_predicate_method)
.include?(node_type)

failure_message = <<~MSG
#{described_class.name}##{method_name} is not documented as it was generated automatically as a fallback.
To fix this, define it using the following macro instead:
class #{described_class.name} < #{described_class.superclass.name}
# ...
def_node_type_predicate :#{node_name}#{unless node_type.to_s == node_name
", :#{node_type}"
end}
MSG

expect(is_documented).to be(true), failure_message
end
end
end
end

0 comments on commit 37a86d1

Please sign in to comment.