rubocop-rspec 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +23 -0
- data/Rakefile +19 -1
- data/config/default.yml +78 -15
- data/lib/rubocop-rspec.rb +19 -1
- data/lib/rubocop/cop/rspec/any_instance.rb +5 -1
- data/lib/rubocop/cop/rspec/be_eql.rb +58 -0
- data/lib/rubocop/cop/rspec/describe_class.rb +2 -3
- data/lib/rubocop/cop/rspec/describe_method.rb +4 -3
- data/lib/rubocop/cop/rspec/described_class.rb +4 -35
- data/lib/rubocop/cop/rspec/empty_example_group.rb +100 -0
- data/lib/rubocop/cop/rspec/example_length.rb +5 -2
- data/lib/rubocop/cop/rspec/example_wording.rb +5 -2
- data/lib/rubocop/cop/rspec/expect_actual.rb +79 -0
- data/lib/rubocop/cop/rspec/file_path.rb +3 -1
- data/lib/rubocop/cop/rspec/focus.rb +12 -28
- data/lib/rubocop/cop/rspec/hook_argument.rb +122 -0
- data/lib/rubocop/cop/rspec/instance_variable.rb +53 -12
- data/lib/rubocop/cop/rspec/leading_subject.rb +58 -0
- data/lib/rubocop/cop/rspec/let_setup.rb +58 -0
- data/lib/rubocop/cop/rspec/message_chain.rb +33 -0
- data/lib/rubocop/cop/rspec/message_expectation.rb +58 -0
- data/lib/rubocop/cop/rspec/multiple_describes.rb +10 -7
- data/lib/rubocop/cop/rspec/multiple_expectations.rb +89 -0
- data/lib/rubocop/cop/rspec/named_subject.rb +10 -1
- data/lib/rubocop/cop/rspec/nested_groups.rb +125 -0
- data/lib/rubocop/cop/rspec/not_to_not.rb +3 -3
- data/lib/rubocop/cop/rspec/subject_stub.rb +136 -0
- data/lib/rubocop/cop/rspec/verified_doubles.rb +4 -1
- data/lib/rubocop/rspec.rb +10 -0
- data/lib/rubocop/rspec/config_formatter.rb +33 -0
- data/lib/rubocop/rspec/description_extractor.rb +35 -0
- data/lib/rubocop/rspec/inject.rb +2 -6
- data/lib/rubocop/rspec/language.rb +73 -0
- data/lib/rubocop/rspec/language/node_pattern.rb +16 -0
- data/lib/rubocop/rspec/spec_only.rb +61 -0
- data/lib/rubocop/rspec/top_level_describe.rb +6 -0
- data/lib/rubocop/rspec/version.rb +1 -1
- data/rubocop-rspec.gemspec +1 -0
- data/spec/project/changelog_spec.rb +81 -0
- data/spec/project/default_config_spec.rb +52 -0
- data/spec/project/project_requires_spec.rb +8 -0
- data/spec/rubocop/cop/rspec/be_eql_spec.rb +59 -0
- data/spec/rubocop/cop/rspec/described_class_spec.rb +2 -2
- data/spec/rubocop/cop/rspec/empty_example_group_spec.rb +79 -0
- data/spec/rubocop/cop/rspec/example_length_spec.rb +50 -30
- data/spec/rubocop/cop/rspec/example_wording_spec.rb +21 -3
- data/spec/rubocop/cop/rspec/expect_actual_spec.rb +136 -0
- data/spec/rubocop/cop/rspec/file_path_spec.rb +48 -71
- data/spec/rubocop/cop/rspec/focus_spec.rb +1 -1
- data/spec/rubocop/cop/rspec/hook_argument_spec.rb +189 -0
- data/spec/rubocop/cop/rspec/instance_variable_spec.rb +37 -0
- data/spec/rubocop/cop/rspec/leading_subject_spec.rb +54 -0
- data/spec/rubocop/cop/rspec/let_setup_spec.rb +66 -0
- data/spec/rubocop/cop/rspec/message_chain_spec.rb +21 -0
- data/spec/rubocop/cop/rspec/message_expectation_spec.rb +63 -0
- data/spec/rubocop/cop/rspec/multiple_expectations_spec.rb +84 -0
- data/spec/rubocop/cop/rspec/nested_groups_spec.rb +55 -0
- data/spec/rubocop/cop/rspec/not_to_not_spec.rb +12 -2
- data/spec/rubocop/cop/rspec/subject_stub_spec.rb +183 -0
- data/spec/rubocop/rspec/config_formatter_spec.rb +48 -0
- data/spec/rubocop/rspec/description_extractor_spec.rb +35 -0
- data/spec/rubocop/rspec/language/selector_set_spec.rb +29 -0
- data/spec/rubocop/rspec/spec_only_spec.rb +97 -0
- data/spec/shared/rspec_only_cop_behavior.rb +68 -0
- data/spec/spec_helper.rb +13 -1
- metadata +72 -5
- data/spec/project_spec.rb +0 -115
@@ -1,8 +1,7 @@
|
|
1
1
|
module RuboCop
|
2
2
|
module Cop
|
3
3
|
module RSpec
|
4
|
-
#
|
5
|
-
# expectations.
|
4
|
+
# Checks for consistent method usage for negating expectations.
|
6
5
|
#
|
7
6
|
# @example
|
8
7
|
# # bad
|
@@ -15,7 +14,8 @@ module RuboCop
|
|
15
14
|
# expect(false).not_to be_true
|
16
15
|
# end
|
17
16
|
class NotToNot < Cop
|
18
|
-
include RuboCop::
|
17
|
+
include RuboCop::RSpec::SpecOnly,
|
18
|
+
RuboCop::Cop::ConfigurableEnforcedStyle
|
19
19
|
|
20
20
|
MSG = 'Prefer `%s` over `%s`'.freeze
|
21
21
|
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module RSpec
|
6
|
+
# Checks for stubbed test subjects.
|
7
|
+
#
|
8
|
+
# @see https://robots.thoughtbot.com/don-t-stub-the-system-under-test
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# # bad
|
12
|
+
# describe Foo do
|
13
|
+
# subject(:bar) { baz }
|
14
|
+
#
|
15
|
+
# before do
|
16
|
+
# allow(bar).to receive(:qux?).and_return(true)
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
class SubjectStub < Cop
|
21
|
+
include RuboCop::RSpec::SpecOnly,
|
22
|
+
RuboCop::RSpec::TopLevelDescribe,
|
23
|
+
RuboCop::RSpec::Language,
|
24
|
+
RuboCop::RSpec::Language::NodePattern
|
25
|
+
|
26
|
+
MSG = 'Do not stub your test subject.'.freeze
|
27
|
+
|
28
|
+
# @!method subject(node)
|
29
|
+
# Find a named or unnamed subject definition
|
30
|
+
#
|
31
|
+
# @example anonymous subject
|
32
|
+
# subject(parse('subject { foo }').ast) do |name|
|
33
|
+
# name # => :subject
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# @example named subject
|
37
|
+
# subject(parse('subject(:thing) { foo }').ast) do |name|
|
38
|
+
# name # => :thing
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# @param node [RuboCop::Node]
|
42
|
+
#
|
43
|
+
# @yield [Symbol] subject name
|
44
|
+
def_node_matcher :subject, <<-PATTERN
|
45
|
+
{
|
46
|
+
(block (send nil :subject (sym $_)) args ...)
|
47
|
+
(block (send nil $:subject) args ...)
|
48
|
+
}
|
49
|
+
PATTERN
|
50
|
+
|
51
|
+
# @!method message_expectation?(node, method_name)
|
52
|
+
# Match `allow` and `expect(...).to receive`
|
53
|
+
#
|
54
|
+
# @example source that matches
|
55
|
+
# allow(foo).to receive(:bar)
|
56
|
+
# allow(foo).to receive(:bar).with(1)
|
57
|
+
# allow(foo).to receive(:bar).with(1).and_return(2)
|
58
|
+
# expect(foo).to receive(:bar)
|
59
|
+
# expect(foo).to receive(:bar).with(1)
|
60
|
+
# expect(foo).to receive(:bar).with(1).and_return(2)
|
61
|
+
def_node_matcher :message_expectation?, <<-PATTERN
|
62
|
+
{
|
63
|
+
(send nil :allow (send nil %))
|
64
|
+
(send (send nil :expect (send nil %)) :to #receive_message?)
|
65
|
+
}
|
66
|
+
PATTERN
|
67
|
+
|
68
|
+
def_node_search :receive_message?, '(send nil :receive ...)'
|
69
|
+
|
70
|
+
def on_block(node)
|
71
|
+
return unless example_group?(node)
|
72
|
+
|
73
|
+
find_subject_stub(node) { |stub| add_offense(stub, :expression) }
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# Find subjects within tree and then find (send) nodes for that subject
|
79
|
+
#
|
80
|
+
# @param node [RuboCop::Node] example group
|
81
|
+
#
|
82
|
+
# @yield [RuboCop::Node] message expectations for subject
|
83
|
+
def find_subject_stub(node, &block)
|
84
|
+
find_subject(node) do |subject_name, context|
|
85
|
+
find_subject_expectation(context, subject_name, &block)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Find a subject message expectation
|
90
|
+
#
|
91
|
+
# @param node [RuboCop::Node]
|
92
|
+
# @param subject_name [Symbol] name of subject
|
93
|
+
#
|
94
|
+
# @yield [RuboCop::Node] message expectation
|
95
|
+
def find_subject_expectation(node, subject_name, &block)
|
96
|
+
# Do not search node if it is an example group with its own subject.
|
97
|
+
return if example_group?(node) && redefines_subject?(node)
|
98
|
+
|
99
|
+
# Yield the current node if it is a message expectation.
|
100
|
+
yield(node) if message_expectation?(node, subject_name)
|
101
|
+
|
102
|
+
# Recurse through node's children looking for a message expectation.
|
103
|
+
node.each_child_node do |child|
|
104
|
+
find_subject_expectation(child, subject_name, &block)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Check if node's children contain a subject definition
|
109
|
+
#
|
110
|
+
# @param node [RuboCop::Node]
|
111
|
+
#
|
112
|
+
# @return [Boolean]
|
113
|
+
def redefines_subject?(node)
|
114
|
+
node.each_child_node.any? do |child|
|
115
|
+
subject(child) || redefines_subject?(child)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Find a subject definition
|
120
|
+
#
|
121
|
+
# @param node [RuboCop::Node]
|
122
|
+
# @param parent [RuboCop::Node,nil]
|
123
|
+
#
|
124
|
+
# @yieldparam subject_name [Symbol] name of subject being defined
|
125
|
+
# @yieldparam parent [RuboCop::Node] parent of subject definition
|
126
|
+
def find_subject(node, parent: nil, &block)
|
127
|
+
subject(node) { |name| yield(name, parent) }
|
128
|
+
|
129
|
+
node.each_child_node do |child|
|
130
|
+
find_subject(child, parent: node, &block)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -4,7 +4,8 @@ module RuboCop
|
|
4
4
|
module Cop
|
5
5
|
module RSpec
|
6
6
|
# Prefer using verifying doubles over normal doubles.
|
7
|
-
#
|
7
|
+
#
|
8
|
+
# @see https://relishapp.com/rspec/rspec-mocks/docs/verifying-doubles
|
8
9
|
#
|
9
10
|
# @example
|
10
11
|
# # bad
|
@@ -17,6 +18,8 @@ module RuboCop
|
|
17
18
|
# widget = instance_double("Widget")
|
18
19
|
# end
|
19
20
|
class VerifiedDoubles < Cop
|
21
|
+
include RuboCop::RSpec::SpecOnly
|
22
|
+
|
20
23
|
MSG = 'Prefer using verifying doubles over normal doubles.'.freeze
|
21
24
|
|
22
25
|
def_node_matcher :unverified_double, <<-PATTERN
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module RuboCop
|
2
|
+
# RuboCop RSpec project namespace
|
3
|
+
module RSpec
|
4
|
+
PROJECT_ROOT = Pathname.new(__dir__).parent.parent.expand_path.freeze
|
5
|
+
CONFIG_DEFAULT = PROJECT_ROOT.join('config', 'default.yml').freeze
|
6
|
+
CONFIG = YAML.load(CONFIG_DEFAULT.read).freeze
|
7
|
+
|
8
|
+
private_constant(*constants(false))
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module RSpec
|
5
|
+
# Builds a YAML config file from two config hashes
|
6
|
+
class ConfigFormatter
|
7
|
+
NAMESPACE = 'RSpec'.freeze
|
8
|
+
|
9
|
+
def initialize(config, descriptions)
|
10
|
+
@config = config
|
11
|
+
@descriptions = descriptions
|
12
|
+
end
|
13
|
+
|
14
|
+
def dump
|
15
|
+
YAML.dump(unified_config).gsub(/^#{NAMESPACE}/, "\n#{NAMESPACE}")
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def unified_config
|
21
|
+
cops.each_with_object(config.dup) do |cop, unified|
|
22
|
+
unified[cop] = config.fetch(cop).merge(descriptions.fetch(cop))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def cops
|
27
|
+
(descriptions.keys + config.keys).uniq.grep(/\A#{NAMESPACE}/)
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :config, :descriptions
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module RuboCop
|
2
|
+
module RSpec
|
3
|
+
# Extracts cop descriptions from YARD docstrings
|
4
|
+
class DescriptionExtractor
|
5
|
+
COP_NAMESPACE = 'RuboCop::Cop::RSpec'.freeze
|
6
|
+
COP_FORMAT = 'RSpec/%s'.freeze
|
7
|
+
|
8
|
+
def initialize(yardocs)
|
9
|
+
@yardocs = yardocs
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_h
|
13
|
+
cop_documentation.each_with_object({}) do |(name, docstring), config|
|
14
|
+
config[format(COP_FORMAT, name)] = {
|
15
|
+
'Description' => docstring.split("\n\n").first.to_s
|
16
|
+
}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def cop_documentation
|
23
|
+
yardocs
|
24
|
+
.select(&method(:cop?))
|
25
|
+
.map { |doc| [doc.name, doc.docstring] }
|
26
|
+
end
|
27
|
+
|
28
|
+
def cop?(doc)
|
29
|
+
doc.type.equal?(:class) && doc.to_s.start_with?(COP_NAMESPACE)
|
30
|
+
end
|
31
|
+
|
32
|
+
attr_reader :yardocs
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/rubocop/rspec/inject.rb
CHANGED
@@ -3,15 +3,11 @@ module RuboCop
|
|
3
3
|
# Because RuboCop doesn't yet support plugins, we have to monkey patch in a
|
4
4
|
# bit of our configuration.
|
5
5
|
module Inject
|
6
|
-
DEFAULT_FILE = File.expand_path(
|
7
|
-
'../../../../config/default.yml', __FILE__
|
8
|
-
)
|
9
|
-
|
10
6
|
def self.defaults!
|
11
|
-
path =
|
7
|
+
path = CONFIG_DEFAULT.to_s
|
12
8
|
hash = ConfigLoader.send(:load_yaml_configuration, path)
|
13
9
|
config = Config.new(hash, path)
|
14
|
-
puts "configuration from #{
|
10
|
+
puts "configuration from #{path}" if ConfigLoader.debug?
|
15
11
|
config = ConfigLoader.merge_with_default(config, path)
|
16
12
|
ConfigLoader.instance_variable_set(:@default_configuration, config)
|
17
13
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module RSpec
|
5
|
+
# RSpec public API methods that are commonly used in cops
|
6
|
+
module Language
|
7
|
+
# Set of method selectors
|
8
|
+
class SelectorSet
|
9
|
+
def initialize(selectors)
|
10
|
+
@selectors = selectors
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
selectors.eql?(other.selectors)
|
15
|
+
end
|
16
|
+
|
17
|
+
def +(other)
|
18
|
+
self.class.new(selectors + other.selectors)
|
19
|
+
end
|
20
|
+
|
21
|
+
def include?(selector)
|
22
|
+
selectors.include?(selector)
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_node_pattern
|
26
|
+
selectors.map(&:inspect).join(' ')
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
attr_reader :selectors
|
32
|
+
end
|
33
|
+
|
34
|
+
module ExampleGroups
|
35
|
+
GROUPS = SelectorSet.new(%i(describe context feature example_group))
|
36
|
+
SKIPPED = SelectorSet.new(%i(xdescribe xcontext xfeature))
|
37
|
+
FOCUSED = SelectorSet.new(%i(fdescribe fcontext ffeature))
|
38
|
+
|
39
|
+
ALL = GROUPS + SKIPPED + FOCUSED
|
40
|
+
end
|
41
|
+
|
42
|
+
module SharedGroups
|
43
|
+
ALL = SelectorSet.new(
|
44
|
+
%i(shared_examples shared_context shared_examples_for)
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
module Examples
|
49
|
+
EXAMPLES = SelectorSet.new(%i(it specify example scenario its))
|
50
|
+
FOCUSED = SelectorSet.new(%i(fit fspecify fexample fscenario focus))
|
51
|
+
SKIPPED = SelectorSet.new(%i(xit xspecify xexample xscenario skip))
|
52
|
+
PENDING = SelectorSet.new(%i(pending))
|
53
|
+
|
54
|
+
ALL = EXAMPLES + FOCUSED + SKIPPED + PENDING
|
55
|
+
end
|
56
|
+
|
57
|
+
module Hooks
|
58
|
+
ALL = SelectorSet.new(%i(after around before))
|
59
|
+
end
|
60
|
+
|
61
|
+
module Helpers
|
62
|
+
ALL = SelectorSet.new(%i(let let!))
|
63
|
+
end
|
64
|
+
|
65
|
+
ALL =
|
66
|
+
ExampleGroups::ALL +
|
67
|
+
SharedGroups::ALL +
|
68
|
+
Examples::ALL +
|
69
|
+
Hooks::ALL +
|
70
|
+
Helpers::ALL
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module RSpec
|
5
|
+
module Language
|
6
|
+
# Common node matchers used for matching against the rspec DSL
|
7
|
+
module NodePattern
|
8
|
+
extend RuboCop::NodePattern::Macros
|
9
|
+
|
10
|
+
def_node_matcher :example_group?, <<-PATTERN
|
11
|
+
(block (send _ {#{ExampleGroups::ALL.to_node_pattern}} ...) ...)
|
12
|
+
PATTERN
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module RSpec
|
5
|
+
# Mixin for cops that skips non-spec files
|
6
|
+
#
|
7
|
+
# The criteria for whether rubocop-rspec analyzes a certain ruby file
|
8
|
+
# is configured via `AllCops/RSpec`. For example, if you want to
|
9
|
+
# customize your project to scan all files within a `test/` directory
|
10
|
+
# then you could add this to your configuration:
|
11
|
+
#
|
12
|
+
# @example configuring analyzed paths
|
13
|
+
#
|
14
|
+
# AllCops:
|
15
|
+
# RSpec:
|
16
|
+
# Patterns:
|
17
|
+
# - '_spec.rb$'
|
18
|
+
# - '(?:^|/)spec/'
|
19
|
+
#
|
20
|
+
# @note this functionality is implemented via this mixin instead of
|
21
|
+
# a subclass of `RuboCop::Cop::Cop` because the `Cop` class assumes
|
22
|
+
# that it will be the direct superclass of all cops. For example,
|
23
|
+
# if the ancestry of a cop looked like this:
|
24
|
+
#
|
25
|
+
# class RuboCop::RSpec::Cop < RuboCop::Cop::Cop
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# class RuboCop::RSpec::SpecCop < RuboCop::RSpec::Cop
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# then `SpecCop` will fail to be registered on the class instance
|
32
|
+
# variable of `Cop` which tracks all descendants via `.inherited`.
|
33
|
+
#
|
34
|
+
# While we could match this behavior and provide a rubocop-rspec Cop
|
35
|
+
# parent class, it would rely heavily on the implementation details
|
36
|
+
# of RuboCop itself which is largly private API. This would be
|
37
|
+
# irresponsible since any patch level release of rubocop could break
|
38
|
+
# integrations for users of rubocop-rspec
|
39
|
+
#
|
40
|
+
module SpecOnly
|
41
|
+
DEFAULT_CONFIGURATION = CONFIG.fetch('AllCops').fetch('RSpec')
|
42
|
+
|
43
|
+
def relevant_file?(file)
|
44
|
+
rspec_pattern =~ file && super
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def rspec_pattern
|
50
|
+
Regexp.union(rspec_pattern_config.map(&Regexp.public_method(:new)))
|
51
|
+
end
|
52
|
+
|
53
|
+
def rspec_pattern_config
|
54
|
+
config
|
55
|
+
.for_all_cops
|
56
|
+
.fetch('RSpec', DEFAULT_CONFIGURATION)
|
57
|
+
.fetch('Patterns')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -2,6 +2,12 @@ module RuboCop
|
|
2
2
|
module RSpec
|
3
3
|
# Helper methods for top level describe cops
|
4
4
|
module TopLevelDescribe
|
5
|
+
extend NodePattern::Macros
|
6
|
+
|
7
|
+
def_node_matcher :described_constant, <<-PATTERN
|
8
|
+
(block $(send _ :describe $(const ...)) (args) $_)
|
9
|
+
PATTERN
|
10
|
+
|
5
11
|
def on_send(node)
|
6
12
|
return unless respond_to?(:on_top_level_describe)
|
7
13
|
return unless top_level_describe?(node)
|