From 37a86d1ba20aa743bc18449f3e2f6a0af335e742 Mon Sep 17 00:00:00 2001 From: Sam Bostock Date: Fri, 8 Nov 2024 15:02:39 -0500 Subject: [PATCH] Fallback to dynamically defining node type predicates 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`. --- lib/rubocop/ast/node.rb | 11 +++++++++++ spec/rubocop/ast/node_spec.rb | 29 +++++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/lib/rubocop/ast/node.rb b/lib/rubocop/ast/node.rb index e9adc1e0d..a8fabb72b 100644 --- a/lib/rubocop/ast/node.rb +++ b/lib/rubocop/ast/node.rb @@ -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) @@ -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 diff --git a/spec/rubocop/ast/node_spec.rb b/spec/rubocop/ast/node_spec.rb index d161e7f6d..5d70dda9e 100644 --- a/spec/rubocop/ast/node_spec.rb +++ b/spec/rubocop/ast/node_spec.rb @@ -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