rubocop-capybara 2.23.0 → 3.1.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 +53 -0
- data/README.md +1 -1
- data/config/default.yml +97 -32
- data/lib/rubocop/capybara/plugin.rb +2 -3
- data/lib/rubocop/capybara/version.rb +1 -1
- data/lib/rubocop/cop/capybara/absolute_x_path_in_element_scope.rb +104 -0
- data/lib/rubocop/cop/capybara/always_true_count_query.rb +75 -0
- data/lib/rubocop/cop/capybara/ambiguous_click.rb +1 -1
- data/lib/rubocop/cop/capybara/assert_style.rb +1 -1
- data/lib/rubocop/cop/capybara/chained_find.rb +175 -0
- data/lib/rubocop/cop/capybara/find_all_first.rb +13 -41
- data/lib/rubocop/cop/capybara/ignored_query_option.rb +242 -0
- data/lib/rubocop/cop/capybara/mixin/capybara_help.rb +86 -2
- data/lib/rubocop/cop/capybara/mixin/css_attributes_parser.rb +23 -5
- data/lib/rubocop/cop/capybara/mixin/css_selector.rb +13 -10
- data/lib/rubocop/cop/capybara/mixin/query_methods.rb +120 -0
- data/lib/rubocop/cop/capybara/mixin/visibility.rb +53 -0
- data/lib/rubocop/cop/capybara/modal_method_without_block.rb +37 -0
- data/lib/rubocop/cop/capybara/native_attribute.rb +76 -0
- data/lib/rubocop/cop/capybara/redundant_within_find.rb +10 -21
- data/lib/rubocop/cop/capybara/rspec/current_path_expectation.rb +13 -16
- data/lib/rubocop/cop/capybara/rspec/current_url_expectation.rb +122 -0
- data/lib/rubocop/cop/capybara/rspec/find_element_value_eq.rb +173 -0
- data/lib/rubocop/cop/capybara/rspec/have_content.rb +1 -1
- data/lib/rubocop/cop/capybara/rspec/have_selector.rb +26 -15
- data/lib/rubocop/cop/capybara/rspec/match_style.rb +1 -1
- data/lib/rubocop/cop/capybara/rspec/negation_matcher.rb +1 -1
- data/lib/rubocop/cop/capybara/rspec/negation_matcher_after_visit.rb +2 -2
- data/lib/rubocop/cop/capybara/rspec/predicate_matcher.rb +43 -7
- data/lib/rubocop/cop/capybara/rspec/specific_matcher.rb +8 -2
- data/lib/rubocop/cop/capybara/rspec/visibility_matcher.rb +29 -24
- data/lib/rubocop/cop/capybara/rspec.rb +25 -0
- data/lib/rubocop/cop/capybara/specific_actions.rb +12 -7
- data/lib/rubocop/cop/capybara/specific_finders.rb +149 -28
- data/lib/rubocop/cop/capybara/unordered_window_access.rb +40 -0
- data/lib/rubocop/cop/capybara/visibility_option.rb +64 -0
- data/lib/rubocop/cop/capybara.rb +28 -0
- data/lib/rubocop/cop/capybara_cops.rb +4 -17
- data/lib/rubocop-capybara.rb +7 -5
- metadata +18 -6
- data/config/obsoletion.yml +0 -18
- data/lib/rubocop/cop/capybara/click_link_or_button_style.rb +0 -87
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Capybara
|
|
6
|
+
# Avoid chaining `find` methods and combine selectors.
|
|
7
|
+
#
|
|
8
|
+
# Combining selectors into a single `find` call is more efficient than
|
|
9
|
+
# chaining multiple `find` calls, as it reduces the number of DOM queries.
|
|
10
|
+
# Only recognizable Capybara finder chains with string locators are
|
|
11
|
+
# checked.
|
|
12
|
+
# Set `DefaultSelector` to match `Capybara.default_selector`, or to
|
|
13
|
+
# `null` if it varies at runtime. Autocorrection requires `css` and
|
|
14
|
+
# literal strings.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # bad
|
|
18
|
+
# page.find('#foo').find('.bar')
|
|
19
|
+
# page.find('#foo').find('.bar').find('.baz')
|
|
20
|
+
#
|
|
21
|
+
# # good
|
|
22
|
+
# page.find('#foo .bar')
|
|
23
|
+
# page.find('#foo .bar .baz')
|
|
24
|
+
# find('#form').find(:button, 'Save')
|
|
25
|
+
#
|
|
26
|
+
class ChainedFind < RuboCop::Cop::Base # rubocop:disable Metrics/ClassLength
|
|
27
|
+
extend AutoCorrector
|
|
28
|
+
include CapybaraHelp
|
|
29
|
+
|
|
30
|
+
MSG = 'Avoid chaining `find` methods. ' \
|
|
31
|
+
'Combine the selectors into a single `find` call.'
|
|
32
|
+
MSG_WITH_COMBINED = 'Use `find(%<combined>s)` instead of ' \
|
|
33
|
+
'chaining `find` methods.'
|
|
34
|
+
RESTRICT_ON_SEND = %i[find].freeze
|
|
35
|
+
|
|
36
|
+
# @!method single_string_find?(node)
|
|
37
|
+
def_node_matcher :single_string_find?, <<~PATTERN
|
|
38
|
+
(call _ :find (str $_))
|
|
39
|
+
PATTERN
|
|
40
|
+
|
|
41
|
+
def on_send(node)
|
|
42
|
+
return unless chained_find?(node)
|
|
43
|
+
|
|
44
|
+
find_receiver = find_receiver(node.receiver)
|
|
45
|
+
return unless find_receiver
|
|
46
|
+
return unless string_locators?(node, find_receiver)
|
|
47
|
+
return unless capybara_receiver?(find_receiver.receiver)
|
|
48
|
+
|
|
49
|
+
register_offense(node, find_receiver)
|
|
50
|
+
end
|
|
51
|
+
alias on_csend on_send
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def chained_find?(node)
|
|
56
|
+
node.method?(:find) && node.receiver
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def string_locators?(node, receiver)
|
|
60
|
+
node.first_argument&.type?(:str, :dstr) &&
|
|
61
|
+
receiver.first_argument&.type?(:str, :dstr)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def find_receiver(receiver)
|
|
65
|
+
return unwrapped_receiver(receiver) if receiver.block_type?
|
|
66
|
+
|
|
67
|
+
receiver if receiver.call_type? && receiver.method?(:find)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def unwrapped_receiver(block_node)
|
|
71
|
+
send_node = block_node.send_node
|
|
72
|
+
send_node if send_node.method?(:find)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def register_offense(node, find_receiver)
|
|
76
|
+
if safe_to_autocorrect?(node, find_receiver)
|
|
77
|
+
add_correctable_offense(node, find_receiver)
|
|
78
|
+
else
|
|
79
|
+
add_offense(node)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def add_correctable_offense(node, find_receiver)
|
|
84
|
+
combined = combined_selector(node, find_receiver)
|
|
85
|
+
message = format(
|
|
86
|
+
MSG_WITH_COMBINED,
|
|
87
|
+
combined: selector_literal(combined)
|
|
88
|
+
)
|
|
89
|
+
return add_offense(node, message: message) if nested_find?(node)
|
|
90
|
+
|
|
91
|
+
add_offense(node, message: message) do |corrector|
|
|
92
|
+
corrector.replace(node, replacement(node, combined))
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def nested_find?(node)
|
|
97
|
+
node.parent&.call_type? &&
|
|
98
|
+
node.parent.method?(:find) &&
|
|
99
|
+
node.parent.receiver == node
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def safe_to_autocorrect?(node, receiver)
|
|
103
|
+
cop_config['DefaultSelector'].to_s == 'css' &&
|
|
104
|
+
single_string_argument?(node) &&
|
|
105
|
+
single_string_argument?(receiver) &&
|
|
106
|
+
node.type == receiver.type &&
|
|
107
|
+
safe_selectors?(node)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def safe_selectors?(node)
|
|
111
|
+
current = node
|
|
112
|
+
while current&.call_type? && current.method?(:find)
|
|
113
|
+
return false unless single_string_argument?(current)
|
|
114
|
+
|
|
115
|
+
return false if unsafe_selector?(current.first_argument.value)
|
|
116
|
+
|
|
117
|
+
current = current.receiver
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
true
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def unsafe_selector?(selector)
|
|
124
|
+
selector.match?(/(?<!\\),/) || selector.start_with?('//', './/')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def single_string_argument?(node)
|
|
128
|
+
node.arguments.one? &&
|
|
129
|
+
!node.block_node &&
|
|
130
|
+
single_string_find?(node)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def combined_selector(node, receiver)
|
|
134
|
+
selectors = collect_selectors(receiver)
|
|
135
|
+
current = node.first_argument.value
|
|
136
|
+
|
|
137
|
+
"#{selectors} #{current}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def collect_selectors(node)
|
|
141
|
+
[].tap do |selectors|
|
|
142
|
+
current = node
|
|
143
|
+
while (selector = single_string_find?(current))
|
|
144
|
+
selectors.unshift(selector)
|
|
145
|
+
current = current.receiver
|
|
146
|
+
break unless current&.call_type? && current.method?(:find)
|
|
147
|
+
end
|
|
148
|
+
end.join(' ')
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def replacement(node, combined_selector)
|
|
152
|
+
root = chain_root(node)
|
|
153
|
+
receiver_source = root.receiver&.source
|
|
154
|
+
find_call = "find(#{selector_literal(combined_selector)})"
|
|
155
|
+
|
|
156
|
+
[receiver_source, find_call].compact.join(root.loc.dot&.source || '.')
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def selector_literal(selector)
|
|
160
|
+
return selector.inspect if selector.match?(/['\\]/)
|
|
161
|
+
|
|
162
|
+
"'#{selector}'"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def chain_root(node)
|
|
166
|
+
root = node
|
|
167
|
+
while root.receiver&.call_type? && root.receiver.method?(:find)
|
|
168
|
+
root = root.receiver
|
|
169
|
+
end
|
|
170
|
+
root
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
@@ -6,29 +6,25 @@ module RuboCop
|
|
|
6
6
|
# Enforces use of `first` instead of `all` with `first` or `[0]`.
|
|
7
7
|
#
|
|
8
8
|
# @safety
|
|
9
|
-
#
|
|
10
|
-
# `
|
|
11
|
-
#
|
|
12
|
-
# with `first` may break code that depends on the return value
|
|
13
|
-
# being a collection (e.g. calling `.each` on the result).
|
|
9
|
+
# With default options, `all(...).first` and `all(...)[0]` return
|
|
10
|
+
# `nil` when no element matches, while `first(...)` raises an
|
|
11
|
+
# exception. Autocorrection can break code that depends on `nil`.
|
|
14
12
|
#
|
|
15
13
|
# @example
|
|
16
14
|
#
|
|
17
15
|
# # bad
|
|
18
16
|
# all('a').first
|
|
19
17
|
# all('a')[0]
|
|
20
|
-
# find('a', match: :first)
|
|
21
|
-
# all('a', match: :first)
|
|
22
18
|
#
|
|
23
19
|
# # good
|
|
24
20
|
# first('a')
|
|
25
21
|
#
|
|
26
|
-
class FindAllFirst <
|
|
22
|
+
class FindAllFirst < RuboCop::Cop::Base
|
|
27
23
|
extend AutoCorrector
|
|
28
24
|
include RangeHelp
|
|
29
25
|
|
|
30
26
|
MSG = 'Use `first(%<selector>s)`.'
|
|
31
|
-
RESTRICT_ON_SEND = %i[all
|
|
27
|
+
RESTRICT_ON_SEND = %i[all].freeze
|
|
32
28
|
|
|
33
29
|
# @!method find_all_first?(node)
|
|
34
30
|
def_node_matcher :find_all_first?, <<~PATTERN
|
|
@@ -38,22 +34,10 @@ module RuboCop
|
|
|
38
34
|
}
|
|
39
35
|
PATTERN
|
|
40
36
|
|
|
41
|
-
# @!method include_match_first?(node)
|
|
42
|
-
def_node_matcher :include_match_first?, <<~PATTERN
|
|
43
|
-
(send _ {:find :all} _ $(hash <(pair (sym :match) (sym :first)) ...>))
|
|
44
|
-
PATTERN
|
|
45
|
-
|
|
46
37
|
def on_send(node)
|
|
47
|
-
on_all_first(node)
|
|
48
|
-
on_match_first(node)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
def on_all_first(node)
|
|
54
38
|
return unless (parent = node.parent)
|
|
55
39
|
return unless find_all_first?(parent)
|
|
56
|
-
return if
|
|
40
|
+
return if nil_sensitive_use?(parent)
|
|
57
41
|
|
|
58
42
|
range = range_between(node.loc.selector.begin_pos,
|
|
59
43
|
parent.loc.selector.end_pos)
|
|
@@ -64,27 +48,15 @@ module RuboCop
|
|
|
64
48
|
end
|
|
65
49
|
end
|
|
66
50
|
|
|
67
|
-
|
|
68
|
-
include_match_first?(node) do |hash|
|
|
69
|
-
selector = ([node.first_argument.source] + replaced_hash(hash))
|
|
70
|
-
.join(', ')
|
|
71
|
-
range = range_between(node.loc.selector.begin_pos,
|
|
72
|
-
node.source_range.end_pos)
|
|
73
|
-
add_offense(range,
|
|
74
|
-
message: format(MSG, selector: selector)) do |corrector|
|
|
75
|
-
corrector.replace(range, "first(#{selector})")
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
51
|
+
private
|
|
79
52
|
|
|
80
|
-
def
|
|
81
|
-
|
|
82
|
-
arg == 'match: :first'
|
|
83
|
-
end
|
|
84
|
-
end
|
|
53
|
+
def nil_sensitive_use?(node)
|
|
54
|
+
return true if node.ancestors.any?(&:operator_keyword?)
|
|
85
55
|
|
|
86
|
-
|
|
87
|
-
|
|
56
|
+
parent = node.parent
|
|
57
|
+
(parent&.csend_type? && parent.receiver == node) ||
|
|
58
|
+
(parent&.type?(:if, :while, :until) &&
|
|
59
|
+
parent.condition == node)
|
|
88
60
|
end
|
|
89
61
|
end
|
|
90
62
|
end
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Capybara
|
|
6
|
+
# Checks for query options that have no effect in Capybara.
|
|
7
|
+
#
|
|
8
|
+
# Count options passed to singular finders are not autocorrected because
|
|
9
|
+
# removing them may conceal the author's intent.
|
|
10
|
+
#
|
|
11
|
+
# This cop targets Capybara's built-in selectors. Set `DefaultSelector`
|
|
12
|
+
# to match `Capybara.default_selector`, or to `null` if it varies at
|
|
13
|
+
# runtime. Queries using custom or unknown selectors are skipped.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# # bad
|
|
17
|
+
# find('.row', count: 3)
|
|
18
|
+
# find(:css, '.item', exact: true)
|
|
19
|
+
# find('.item', text: /foo/, exact_text: true)
|
|
20
|
+
# all(:css, '.item', match: :first)
|
|
21
|
+
# expect(page).to have_css('.item', count: 3, minimum: 1)
|
|
22
|
+
#
|
|
23
|
+
# # good
|
|
24
|
+
# all('.row', count: 3)
|
|
25
|
+
# find(:css, '.item')
|
|
26
|
+
# find('.item', text: /foo/)
|
|
27
|
+
# all(:css, '.item')
|
|
28
|
+
# expect(page).to have_css('.item', count: 3)
|
|
29
|
+
#
|
|
30
|
+
# @example DefaultSelector: custom
|
|
31
|
+
# # bad
|
|
32
|
+
# find(:css, '.item', text: /foo/, exact_text: true)
|
|
33
|
+
#
|
|
34
|
+
# # good
|
|
35
|
+
# find('.item', text: /foo/, exact_text: true)
|
|
36
|
+
#
|
|
37
|
+
class IgnoredQueryOption < RuboCop::Cop::Base # rubocop:disable Metrics/ClassLength
|
|
38
|
+
extend AutoCorrector
|
|
39
|
+
include RangeHelp
|
|
40
|
+
include CapybaraHelp
|
|
41
|
+
|
|
42
|
+
RESTRICT_ON_SEND = QueryMethods.names(
|
|
43
|
+
:finder, :collection, :selector, :match, :text
|
|
44
|
+
).freeze
|
|
45
|
+
COUNT_OPTIONS = %i[count minimum maximum between].freeze
|
|
46
|
+
MATCH_STRATEGIES = %i[first smart prefer_exact one].freeze
|
|
47
|
+
|
|
48
|
+
FIND_COUNT_MSG =
|
|
49
|
+
'`%<option>s` has no effect when passed to `%<method>s`.'
|
|
50
|
+
EXACT_MSG = '`exact` has no effect on CSS queries.'
|
|
51
|
+
EXACT_TEXT_MSG =
|
|
52
|
+
'`exact_text` has no effect when `text` is a regexp.'
|
|
53
|
+
MATCH_MSG =
|
|
54
|
+
'`match` has no effect when passed to `%<method>s`.'
|
|
55
|
+
SHADOWED_COUNT_MSG =
|
|
56
|
+
'`%<option>s` has no effect when `count` is specified.'
|
|
57
|
+
|
|
58
|
+
# @!method query_options(node)
|
|
59
|
+
def_node_matcher :query_options, <<~PATTERN
|
|
60
|
+
(call #capybara_receiver? _ <$(hash ...) ...>)
|
|
61
|
+
PATTERN
|
|
62
|
+
|
|
63
|
+
# @!method custom_filters?(node)
|
|
64
|
+
def_node_matcher :custom_filters?, <<~PATTERN
|
|
65
|
+
(hash <{(pair (sym {:filter_set :session_options}) _) (pair !sym _) kwsplat} ...>)
|
|
66
|
+
PATTERN
|
|
67
|
+
|
|
68
|
+
# @!method removable_value?(node)
|
|
69
|
+
def_node_matcher :removable_value?, <<~PATTERN
|
|
70
|
+
{basic_literal? (range {int nil?} {int nil?})}
|
|
71
|
+
PATTERN
|
|
72
|
+
|
|
73
|
+
def on_send(node)
|
|
74
|
+
return unless (options = query_options(node))
|
|
75
|
+
return if options.braces?
|
|
76
|
+
return if custom_filters?(options)
|
|
77
|
+
|
|
78
|
+
issues = options.pairs.filter_map do |pair|
|
|
79
|
+
issue_for(node, options, pair)
|
|
80
|
+
end
|
|
81
|
+
return if issues.empty?
|
|
82
|
+
|
|
83
|
+
register_issues(node, options, issues)
|
|
84
|
+
end
|
|
85
|
+
alias on_csend on_send
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def register_issues(node, options, issues)
|
|
90
|
+
correction_pair = issues.find do |pair, _, correctable|
|
|
91
|
+
correctable && removable?(options, pair) && !comments_in?(node)
|
|
92
|
+
end
|
|
93
|
+
issues.each do |pair, message, _|
|
|
94
|
+
add_offense(pair, message: message) do |corrector|
|
|
95
|
+
next unless pair == correction_pair&.first
|
|
96
|
+
|
|
97
|
+
remove_option(corrector, node, options, pair)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def issue_for(node, options, pair)
|
|
103
|
+
option = pair.key.value
|
|
104
|
+
issue = find_count_issue(node, option) ||
|
|
105
|
+
exact_issue(node, option) ||
|
|
106
|
+
exact_text_issue(node, option, pair, options) ||
|
|
107
|
+
match_issue(node, option, pair) ||
|
|
108
|
+
shadowed_count_issue(node, option, options)
|
|
109
|
+
|
|
110
|
+
[pair, *issue] if issue
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def find_count_issue(node, option)
|
|
114
|
+
return unless QueryMethods.type(node.method_name) == :finder
|
|
115
|
+
return unless COUNT_OPTIONS.include?(option)
|
|
116
|
+
return if custom_selector?(node)
|
|
117
|
+
|
|
118
|
+
[
|
|
119
|
+
format(FIND_COUNT_MSG,
|
|
120
|
+
option: option,
|
|
121
|
+
method: node.method_name),
|
|
122
|
+
false
|
|
123
|
+
]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def shadowed_count_issue(node, option, options)
|
|
127
|
+
return if QueryMethods.type(node.method_name) == :match
|
|
128
|
+
return unless count_query_with_known_selector?(node)
|
|
129
|
+
return unless shadowed_count?(option, options)
|
|
130
|
+
|
|
131
|
+
[format(SHADOWED_COUNT_MSG, option: option), true]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def exact_issue(node, option)
|
|
135
|
+
[EXACT_MSG, true] if option == :exact && css_query?(node)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def exact_text_issue(node, option, pair, options)
|
|
139
|
+
return unless option == :exact_text
|
|
140
|
+
return unless ineffective_exact_text?(node, pair, options)
|
|
141
|
+
|
|
142
|
+
[EXACT_TEXT_MSG, true]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def match_issue(node, option, pair)
|
|
146
|
+
return unless option == :match && ineffective_match?(node, pair)
|
|
147
|
+
|
|
148
|
+
[format(MATCH_MSG, method: node.method_name), true]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def css_query?(node)
|
|
152
|
+
selector = QueryMethods.selector(node.method_name)
|
|
153
|
+
selector == :css ||
|
|
154
|
+
(selector == :selector && explicit_selector(node) == :css)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def explicit_selector(node)
|
|
158
|
+
argument = node.first_argument
|
|
159
|
+
argument.value if argument&.sym_type?
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def ineffective_exact_text?(node, pair, options)
|
|
163
|
+
exact_text_supported_call?(node) &&
|
|
164
|
+
effective_option_pair(options, :exact_text) == pair &&
|
|
165
|
+
pair.value.type?(:boolean) &&
|
|
166
|
+
effective_option_pair(options, :text)&.value&.regexp_type?
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def exact_text_supported_call?(node)
|
|
170
|
+
QueryMethods.type(node.method_name) != :text &&
|
|
171
|
+
!custom_selector?(node)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def ineffective_match?(node, pair)
|
|
175
|
+
QueryMethods.type(node.method_name) == :collection &&
|
|
176
|
+
QueryMethods.built_in_selector?(explicit_selector(node)) &&
|
|
177
|
+
pair.value.sym_type? &&
|
|
178
|
+
MATCH_STRATEGIES.include?(pair.value.value)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def shadowed_count?(option, options)
|
|
182
|
+
option != :count && COUNT_OPTIONS.include?(option) &&
|
|
183
|
+
count_specified?(options)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def count_query_with_known_selector?(node)
|
|
187
|
+
selector = QueryMethods.selector(node.method_name)
|
|
188
|
+
return true unless selector == :selector
|
|
189
|
+
|
|
190
|
+
QueryMethods.built_in_selector?(explicit_selector(node))
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def custom_selector?(node)
|
|
194
|
+
QueryMethods.selector(node.method_name) == :selector &&
|
|
195
|
+
!QueryMethods.built_in_selector?(query_selector(node))
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def query_selector(node)
|
|
199
|
+
selector = explicit_selector(node)
|
|
200
|
+
return selector if selector
|
|
201
|
+
|
|
202
|
+
positional = node.arguments.reject do |arg|
|
|
203
|
+
arg.type?(:hash, :block_pass)
|
|
204
|
+
end
|
|
205
|
+
return if positional.length > 1
|
|
206
|
+
|
|
207
|
+
cop_config['DefaultSelector'].to_s.to_sym
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def count_specified?(options)
|
|
211
|
+
effective_option_pair(options, :count)&.value&.truthy_literal?
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def effective_option_pair(options, name)
|
|
215
|
+
options.pairs.reverse_each.find { |pair| pair.key.value == name }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def comments_in?(node)
|
|
219
|
+
processed_source.comments.any? do |comment|
|
|
220
|
+
comment.loc.line.between?(node.first_line, node.last_line)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def removable?(options, pair)
|
|
225
|
+
return false unless removable_value?(pair.value)
|
|
226
|
+
return false if pair.value.str_type? && pair.value.heredoc?
|
|
227
|
+
|
|
228
|
+
options.pairs.one? { |other| other.key == pair.key }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def remove_option(corrector, node, options, pair)
|
|
232
|
+
first = node.first_argument == options &&
|
|
233
|
+
options.children.first == pair
|
|
234
|
+
side = first ? :right : :left
|
|
235
|
+
range = range_with_surrounding_space(pair.source_range, side: side)
|
|
236
|
+
range = range_with_surrounding_comma(range, side)
|
|
237
|
+
corrector.remove(range_with_surrounding_space(range, side: side))
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
@@ -5,11 +5,13 @@ module RuboCop
|
|
|
5
5
|
module Capybara
|
|
6
6
|
# Help methods for capybara.
|
|
7
7
|
# @api private
|
|
8
|
-
module CapybaraHelp
|
|
8
|
+
module CapybaraHelp # rubocop:disable Metrics/ModuleLength
|
|
9
|
+
extend RuboCop::AST::NodePattern::Macros
|
|
10
|
+
|
|
9
11
|
CAPYBARA_MATCHERS = %w[
|
|
10
12
|
selector css xpath text title current_path link button
|
|
11
13
|
field checked_field unchecked_field select table
|
|
12
|
-
sibling ancestor content
|
|
14
|
+
sibling ancestor content element
|
|
13
15
|
].freeze
|
|
14
16
|
POSITIVE_MATCHERS =
|
|
15
17
|
Set.new(CAPYBARA_MATCHERS) { |element| :"have_#{element}" }.freeze
|
|
@@ -45,9 +47,91 @@ module RuboCop
|
|
|
45
47
|
SPECIFIC_PSEUDO_CLASSES = %w[
|
|
46
48
|
not() disabled enabled checked unchecked
|
|
47
49
|
].freeze
|
|
50
|
+
VISIBILITY_MATCHER_METHODS = %w[
|
|
51
|
+
button checked_field css field link select selector table
|
|
52
|
+
unchecked_field xpath
|
|
53
|
+
].flat_map do |element|
|
|
54
|
+
[:"have_#{element}", :"have_no_#{element}"]
|
|
55
|
+
end.freeze
|
|
56
|
+
VISIBILITY_FALSE_MESSAGE = 'Use `:all` or `:hidden` instead of `false`.'
|
|
57
|
+
VISIBILITY_TRUE_MESSAGE = 'Use `:visible` instead of `true`.'
|
|
48
58
|
|
|
49
59
|
module_function
|
|
50
60
|
|
|
61
|
+
# Recognize the Capybara DSL and finder chains, not arbitrary objects
|
|
62
|
+
# with methods such as Enumerable#find or ActiveRecord#first.
|
|
63
|
+
# Recursive references intentionally use the pattern-only matcher.
|
|
64
|
+
# Resolving local variables here would recurse forever for assignments
|
|
65
|
+
# such as `tr = tr.find(...)`.
|
|
66
|
+
# @!method capybara_receiver_pattern?(node)
|
|
67
|
+
def_node_matcher :capybara_receiver_pattern?, <<~PATTERN
|
|
68
|
+
{nil? self
|
|
69
|
+
(call {nil? self} :page)
|
|
70
|
+
(call (const {nil? cbase} :Capybara) :current_session)
|
|
71
|
+
(call #capybara_receiver_pattern?
|
|
72
|
+
{:all :ancestor :find :find_all :find_button :find_by_id
|
|
73
|
+
:find_field :find_link :first :sibling} ...)
|
|
74
|
+
(any_block #capybara_receiver_pattern? ...)
|
|
75
|
+
(begin #capybara_receiver_pattern?) }
|
|
76
|
+
PATTERN
|
|
77
|
+
module_function :capybara_receiver_pattern?
|
|
78
|
+
|
|
79
|
+
# @param node [RuboCop::AST::Node]
|
|
80
|
+
# @return [Boolean]
|
|
81
|
+
def capybara_receiver?(node)
|
|
82
|
+
capybara_receiver_pattern?(node) ||
|
|
83
|
+
local_variable_capybara_receiver?(node)
|
|
84
|
+
end
|
|
85
|
+
module_function :capybara_receiver?
|
|
86
|
+
|
|
87
|
+
# ponytail: Only preceding direct assignments in visible lexical scopes;
|
|
88
|
+
# use data-flow analysis if coverage grows.
|
|
89
|
+
def local_variable_capybara_receiver?(node)
|
|
90
|
+
value = local_variable_value(node)
|
|
91
|
+
value && capybara_receiver_pattern?(value)
|
|
92
|
+
end
|
|
93
|
+
module_function :local_variable_capybara_receiver?
|
|
94
|
+
|
|
95
|
+
def local_variable_value(node)
|
|
96
|
+
return unless node.lvar_type?
|
|
97
|
+
|
|
98
|
+
name = node.children.first
|
|
99
|
+
scopes = node.each_ancestor.take_while do |scope|
|
|
100
|
+
outer_assignment_visible_through?(scope, name)
|
|
101
|
+
end
|
|
102
|
+
assignment = scopes.filter_map do |scope|
|
|
103
|
+
local_variable_assignment(node, scope) if scope.begin_type?
|
|
104
|
+
end.first
|
|
105
|
+
assignment && assignment.children[1]
|
|
106
|
+
end
|
|
107
|
+
module_function :local_variable_value
|
|
108
|
+
|
|
109
|
+
def outer_assignment_visible_through?(scope, name)
|
|
110
|
+
return false if scope.type?(:any_def, :class, :module, :sclass)
|
|
111
|
+
|
|
112
|
+
!scope.block_type? ||
|
|
113
|
+
!shadowed_block_variable?(scope.arguments, name)
|
|
114
|
+
end
|
|
115
|
+
module_function :outer_assignment_visible_through?
|
|
116
|
+
|
|
117
|
+
def shadowed_block_variable?(arguments, name)
|
|
118
|
+
args = arguments.each_node(
|
|
119
|
+
:arg, :optarg, :restarg, :kwarg,
|
|
120
|
+
:kwoptarg, :kwrestarg, :blockarg, :shadowarg
|
|
121
|
+
)
|
|
122
|
+
args.any? { |arg| arg.children.first == name }
|
|
123
|
+
end
|
|
124
|
+
module_function :shadowed_block_variable?
|
|
125
|
+
|
|
126
|
+
def local_variable_assignment(node, scope)
|
|
127
|
+
scope.children.reverse.find do |child|
|
|
128
|
+
child.lvasgn_type? &&
|
|
129
|
+
child.children.first == node.children.first &&
|
|
130
|
+
child.source_range.end_pos <= node.source_range.begin_pos
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
module_function :local_variable_assignment
|
|
134
|
+
|
|
51
135
|
# @param node [RuboCop::AST::SendNode]
|
|
52
136
|
# @param locator [String]
|
|
53
137
|
# @param element [String]
|
|
@@ -6,6 +6,8 @@ module RuboCop
|
|
|
6
6
|
# Css selector parser.
|
|
7
7
|
# @api private
|
|
8
8
|
class CssAttributesParser
|
|
9
|
+
QUOTE_CHARS = ['"', "'"].freeze
|
|
10
|
+
|
|
9
11
|
def initialize(selector)
|
|
10
12
|
@selector = selector
|
|
11
13
|
@state = :initial
|
|
@@ -14,7 +16,7 @@ module RuboCop
|
|
|
14
16
|
@bracket_count = 0
|
|
15
17
|
end
|
|
16
18
|
|
|
17
|
-
# @return [
|
|
19
|
+
# @return [Hash<String, String, Boolean, nil>]
|
|
18
20
|
def parse # rubocop:disable Metrics/MethodLength
|
|
19
21
|
@selector.each_char do |char|
|
|
20
22
|
if char == '['
|
|
@@ -43,7 +45,7 @@ module RuboCop
|
|
|
43
45
|
@bracket_count -= 1
|
|
44
46
|
if @bracket_count.zero?
|
|
45
47
|
@state = :initial
|
|
46
|
-
key, value = @temp.split('=')
|
|
48
|
+
key, value = @temp.split('=', 2)
|
|
47
49
|
@results[key] = normalize_value(value)
|
|
48
50
|
@temp.clear
|
|
49
51
|
else
|
|
@@ -52,20 +54,36 @@ module RuboCop
|
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
# @param value [String]
|
|
55
|
-
# @return [Boolean, String]
|
|
57
|
+
# @return [Boolean, String, nil]
|
|
56
58
|
# @example
|
|
57
59
|
# normalize_value('true') # => true
|
|
58
60
|
# normalize_value('false') # => false
|
|
59
61
|
# normalize_value(nil) # => nil
|
|
60
|
-
# normalize_value("foo") # => "
|
|
62
|
+
# normalize_value("foo") # => "foo"
|
|
63
|
+
# normalize_value("'foo'") # => "foo"
|
|
61
64
|
def normalize_value(value)
|
|
65
|
+
return if value&.match?(/\s+[is]\s*\z/i)
|
|
66
|
+
|
|
62
67
|
case value
|
|
63
68
|
when 'true' then true
|
|
64
69
|
when 'false' then false
|
|
65
70
|
when nil then nil
|
|
66
|
-
else
|
|
71
|
+
else strip_outer_quotes(value)
|
|
67
72
|
end
|
|
68
73
|
end
|
|
74
|
+
|
|
75
|
+
def strip_outer_quotes(value)
|
|
76
|
+
return value unless quoted?(value)
|
|
77
|
+
|
|
78
|
+
value[1...-1]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def quoted?(value)
|
|
82
|
+
return false if value.length < 2
|
|
83
|
+
|
|
84
|
+
quote = value[0]
|
|
85
|
+
QUOTE_CHARS.include?(quote) && value.end_with?(quote)
|
|
86
|
+
end
|
|
69
87
|
end
|
|
70
88
|
end
|
|
71
89
|
end
|