From 78c7935f1d52394dae91a905c669be758b381fc4 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 | 38 +++++++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/lib/rubocop/ast/node.rb b/lib/rubocop/ast/node.rb index e9adc1e0d..1b6fa44cd 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. + # 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..02958d52d 100644 --- a/spec/rubocop/ast/node_spec.rb +++ b/spec/rubocop/ast/node_spec.rb @@ -1110,13 +1110,43 @@ 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 + expect(node_type_predicate_is_documented?(node_type)).to( + be(true), + missing_documentation_failure_message(method_name, node_name, node_type) + ) + end + + private + + def node_type_predicate_is_documented?(node_type) + described_class + .instance_variable_get(:@node_types_with_documented_predicate_method) + .include?(node_type) + end + + def missing_documentation_failure_message(method_name, node_name, node_type) + name_matches_type = node_type.to_s == node_name + + <<~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}#{", :#{node_type}" unless name_matches_type} + MSG + end end end end