Skip to content

Commit d5f88f3

Browse files
committed
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`.
1 parent 3470a3c commit d5f88f3

File tree

2 files changed

+45
-4
lines changed

2 files changed

+45
-4
lines changed

lib/rubocop/ast/node.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,15 @@ def #{recursive_kind} # def recursive_litera
139139

140140
# Define a +_type?+ predicate method for the given node type.
141141
private_class_method def self.def_node_type_predicate(name, type = name)
142+
@node_types_with_documented_predicate_method << type
143+
142144
class_eval <<~RUBY, __FILE__, __LINE__ + 1
143145
def #{name}_type? # def block_type?
144146
@type == :#{type} # @type == :block
145147
end # end
146148
RUBY
147149
end
150+
@node_types_with_documented_predicate_method = []
148151

149152
# @see https://www.rubydoc.info/gems/ast/AST/Node:initialize
150153
def initialize(type, children = EMPTY_CHILDREN, properties = EMPTY_PROPERTIES)
@@ -317,6 +320,14 @@ def send_type?
317320
# separately to make this check as fast as possible.
318321
false
319322
end
323+
@node_types_with_documented_predicate_method << :send
324+
325+
# Ensure forward compatibility with new node types, by defining methods for unknown node types too.
326+
# Note these won't get auto-generated documentation, which is why we prefer defining them above.
327+
(Parser::Meta::NODE_TYPES - @node_types_with_documented_predicate_method).each do |node_type|
328+
method_name = :"#{node_type.to_s.gsub(/\W/, '')}_type?"
329+
define_method(method_name) { false }
330+
end
320331

321332
# @!endgroup
322333

spec/rubocop/ast/node_spec.rb

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,13 +1110,43 @@ class << expr
11101110
end
11111111
end
11121112

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

1117-
it "is not of #{method_name}" do
1117+
describe "##{method_name}" do
1118+
it 'is false' do
11181119
expect(described_class.allocate.public_send(method_name)).to be(false)
11191120
end
1121+
1122+
it 'is documented' do
1123+
expect(node_type_predicate_is_documented?(node_type)).to(
1124+
be(true),
1125+
missing_documentation_failure_message(method_name, node_name, node_type)
1126+
)
1127+
end
1128+
1129+
private
1130+
1131+
def node_type_predicate_is_documented?(node_type)
1132+
described_class
1133+
.instance_variable_get(:@node_types_with_documented_predicate_method)
1134+
.include?(node_type)
1135+
end
1136+
1137+
def missing_documentation_failure_message(method_name, node_name, node_type)
1138+
name_matches_type = node_type.to_s == node_name
1139+
1140+
<<~MSG
1141+
#{described_class.name}##{method_name} is not documented as it was generated automatically as a fallback.
1142+
1143+
To fix this, define it using the following macro instead:
1144+
1145+
class #{described_class.name} < #{described_class.superclass.name}
1146+
# ...
1147+
def_node_type_predicate :#{node_name}#{", :#{node_type}" unless name_matches_type}
1148+
MSG
1149+
end
11201150
end
11211151
end
11221152
end

0 commit comments

Comments
 (0)