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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +53 -0
  3. data/README.md +1 -1
  4. data/config/default.yml +97 -32
  5. data/lib/rubocop/capybara/plugin.rb +2 -3
  6. data/lib/rubocop/capybara/version.rb +1 -1
  7. data/lib/rubocop/cop/capybara/absolute_x_path_in_element_scope.rb +104 -0
  8. data/lib/rubocop/cop/capybara/always_true_count_query.rb +75 -0
  9. data/lib/rubocop/cop/capybara/ambiguous_click.rb +1 -1
  10. data/lib/rubocop/cop/capybara/assert_style.rb +1 -1
  11. data/lib/rubocop/cop/capybara/chained_find.rb +175 -0
  12. data/lib/rubocop/cop/capybara/find_all_first.rb +13 -41
  13. data/lib/rubocop/cop/capybara/ignored_query_option.rb +242 -0
  14. data/lib/rubocop/cop/capybara/mixin/capybara_help.rb +86 -2
  15. data/lib/rubocop/cop/capybara/mixin/css_attributes_parser.rb +23 -5
  16. data/lib/rubocop/cop/capybara/mixin/css_selector.rb +13 -10
  17. data/lib/rubocop/cop/capybara/mixin/query_methods.rb +120 -0
  18. data/lib/rubocop/cop/capybara/mixin/visibility.rb +53 -0
  19. data/lib/rubocop/cop/capybara/modal_method_without_block.rb +37 -0
  20. data/lib/rubocop/cop/capybara/native_attribute.rb +76 -0
  21. data/lib/rubocop/cop/capybara/redundant_within_find.rb +10 -21
  22. data/lib/rubocop/cop/capybara/rspec/current_path_expectation.rb +13 -16
  23. data/lib/rubocop/cop/capybara/rspec/current_url_expectation.rb +122 -0
  24. data/lib/rubocop/cop/capybara/rspec/find_element_value_eq.rb +173 -0
  25. data/lib/rubocop/cop/capybara/rspec/have_content.rb +1 -1
  26. data/lib/rubocop/cop/capybara/rspec/have_selector.rb +26 -15
  27. data/lib/rubocop/cop/capybara/rspec/match_style.rb +1 -1
  28. data/lib/rubocop/cop/capybara/rspec/negation_matcher.rb +1 -1
  29. data/lib/rubocop/cop/capybara/rspec/negation_matcher_after_visit.rb +2 -2
  30. data/lib/rubocop/cop/capybara/rspec/predicate_matcher.rb +43 -7
  31. data/lib/rubocop/cop/capybara/rspec/specific_matcher.rb +8 -2
  32. data/lib/rubocop/cop/capybara/rspec/visibility_matcher.rb +29 -24
  33. data/lib/rubocop/cop/capybara/rspec.rb +25 -0
  34. data/lib/rubocop/cop/capybara/specific_actions.rb +12 -7
  35. data/lib/rubocop/cop/capybara/specific_finders.rb +149 -28
  36. data/lib/rubocop/cop/capybara/unordered_window_access.rb +40 -0
  37. data/lib/rubocop/cop/capybara/visibility_option.rb +64 -0
  38. data/lib/rubocop/cop/capybara.rb +28 -0
  39. data/lib/rubocop/cop/capybara_cops.rb +4 -17
  40. data/lib/rubocop-capybara.rb +7 -5
  41. metadata +18 -6
  42. data/config/obsoletion.yml +0 -18
  43. 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
- # This cop's autocorrection is unsafe because `all` returns a
10
- # `Capybara::Result` (an enumerable collection), while `first`
11
- # returns a single `Capybara::Node::Element`. Replacing `all`
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 < ::RuboCop::Cop::Base
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 find].freeze
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 part_of_logical_operator?(parent)
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
- def on_match_first(node)
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 replaced_hash(hash)
81
- hash.child_nodes.flat_map(&:source).reject do |arg|
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
- def part_of_logical_operator?(node)
87
- node.ancestors.any?(&:operator_keyword?)
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 [Array<String>]
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") # => "'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 "'#{value.gsub(/"|'/, '')}'"
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