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
@@ -8,6 +8,8 @@ module RuboCop
8
8
  module CssSelector
9
9
  module_function
10
10
 
11
+ ID_PATTERN = /\A#((?:[\w-]|\\[.>,+~])+)(?:\.|\z)/.freeze
12
+
11
13
  # @param selector [String]
12
14
  # @return [String]
13
15
  # @example
@@ -15,9 +17,7 @@ module RuboCop
15
17
  # id('.some-cls') # => nil
16
18
  # id('#some-id.cls') # => some-id
17
19
  def id(selector)
18
- return unless id?(selector)
19
-
20
- selector.delete('#').gsub(selector.scan(/[^\\]([>,+~.].*)/).join, '')
20
+ selector[ID_PATTERN, 1]
21
21
  end
22
22
 
23
23
  # @param selector [String]
@@ -26,18 +26,21 @@ module RuboCop
26
26
  # id?('#some-id') # => true
27
27
  # id?('.some-cls') # => false
28
28
  def id?(selector)
29
- selector.start_with?('#')
29
+ !id(selector).nil?
30
30
  end
31
31
 
32
32
  # @param selector [String]
33
- # @return [Array<String>]
33
+ # @return [Hash<String, String, Boolean, nil>]
34
34
  # @example
35
35
  # classes('#some-id') # => []
36
36
  # classes('.some-cls') # => ['some-cls']
37
37
  # classes('#some-id.some-cls') # => ['some-cls']
38
38
  # classes('#some-id.cls1.cls2') # => ['cls1', 'cls2']
39
+ # classes('.some-cls\.with-dot') # => ['some-cls.with-dot']
39
40
  def classes(selector)
40
- selector.scan(/\.([\w-]*)/).flatten
41
+ selector.scan(/(?<!\\)\.((?:\\.|[\w-])+)/).flatten.map do |class_name|
42
+ class_name.gsub('\\.', '.')
43
+ end
41
44
  end
42
45
 
43
46
  # @param selector [String]
@@ -50,12 +53,12 @@ module RuboCop
50
53
  end
51
54
 
52
55
  # @param selector [String]
53
- # @return [Array<String>]
56
+ # @return [Hash<String, String, Boolean, nil>]
54
57
  # @example
55
58
  # attributes('a[foo-bar_baz]') # => {"foo-bar_baz=>nil}
56
- # attributes('button[foo][bar=baz]') # => {"foo"=>nil, "bar"=>"'baz'"}
57
- # attributes('table[foo=bar]') # => {"foo"=>"'bar'"}
58
- # attributes('[foo="bar[baz][qux]"]') # => {"foo"=>"'bar[baz][qux]'"}
59
+ # attributes('button[foo][bar=baz]') # => {"foo"=>nil, "bar"=>"baz"}
60
+ # attributes('table[foo=bar]') # => {"foo"=>"bar"}
61
+ # attributes('[foo="bar[baz][qux]"]') # => {"foo"=>"bar[baz][qux]"}
59
62
  def attributes(selector)
60
63
  CssAttributesParser.new(selector).parse
61
64
  end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Capybara
6
+ # Classification of Capybara's public query methods.
7
+ # @api private
8
+ module QueryMethods
9
+ BUILT_IN_SELECTORS = %i[
10
+ button checkbox css datalist_input datalist_option element field
11
+ fieldset file_field fillable_field frame id label link link_or_button
12
+ option radio_button select table table_row xpath
13
+ ].freeze
14
+
15
+ # Each entry contains the query type and its selector. `:selector`
16
+ # means the selector is an argument; nil means it is not applicable.
17
+ METHODS = begin
18
+ methods = {
19
+ find: %i[finder selector],
20
+ ancestor: %i[finder selector],
21
+ sibling: %i[finder selector],
22
+ find_button: %i[finder button],
23
+ find_by_id: %i[finder id],
24
+ find_field: %i[finder field],
25
+ find_link: %i[finder link],
26
+ within: %i[finder selector]
27
+ }
28
+ %i[all find_all first].each do |name|
29
+ methods[name] = %i[collection selector]
30
+ end
31
+
32
+ %w[
33
+ ancestor button checked_field css element field link select
34
+ selector sibling table unchecked_field xpath
35
+ ].each do |selector|
36
+ kind = case selector
37
+ when 'ancestor', 'sibling' then :selector
38
+ when 'checked_field', 'unchecked_field' then :field
39
+ else selector.to_sym
40
+ end
41
+ prefixes = %w[assert assert_no refute has has_no have have_no]
42
+ prefixes += %w[must_have wont_have] unless selector == 'element'
43
+ prefixes.each do |prefix|
44
+ name = "#{prefix}_#{selector}"
45
+ name += '?' if prefix.start_with?('has')
46
+ methods[name.to_sym] = [:selector, kind]
47
+ end
48
+ end
49
+
50
+ %w[css selector xpath].each do |selector|
51
+ %W[
52
+ assert_matches_#{selector} assert_not_matches_#{selector}
53
+ refute_matches_#{selector} matches_#{selector}?
54
+ not_matches_#{selector}? match_#{selector} not_match_#{selector}
55
+ must_match_#{selector} wont_match_#{selector}
56
+ ].each { |name| methods[name.to_sym] = [:match, selector.to_sym] }
57
+ end
58
+
59
+ %w[text content].each do |kind|
60
+ %W[
61
+ assert_#{kind} assert_no_#{kind} refute_#{kind}
62
+ has_#{kind}? has_no_#{kind}? have_#{kind} have_no_#{kind}
63
+ must_have_#{kind} wont_have_#{kind}
64
+ ].each { |name| methods[name.to_sym] = [:text, nil] }
65
+ end
66
+
67
+ %i[
68
+ attach_file check choose click_button click_link
69
+ click_link_or_button click_on fill_in select uncheck unselect
70
+ ].each { |name| methods[name] = [:action, nil] }
71
+
72
+ %w[assert have must_have].product(%w[all any none]) do |prefix, count|
73
+ methods[:"#{prefix}_#{count}_of_selectors"] = %i[grouped selector]
74
+ end
75
+
76
+ methods.transform_values(&:freeze).freeze
77
+ end
78
+ private_constant :METHODS, :BUILT_IN_SELECTORS
79
+
80
+ module_function
81
+
82
+ # @param types [Array<Symbol>]
83
+ # @return [Array<Symbol>]
84
+ # @example
85
+ # names(:collection) # => [:all, :find_all, :first]
86
+ def names(*types)
87
+ METHODS.filter_map { |name, (type, _)| name if types.include?(type) }
88
+ end
89
+
90
+ # @param method_name [Symbol]
91
+ # @return [Symbol, nil]
92
+ # @example
93
+ # type(:find) # => :finder
94
+ # type(:unknown) # => nil
95
+ def type(method_name)
96
+ METHODS[method_name]&.first
97
+ end
98
+
99
+ # @param method_name [Symbol]
100
+ # @return [Symbol, nil]
101
+ # @example
102
+ # selector(:find_button) # => :button
103
+ # selector(:find) # => :selector
104
+ # selector(:have_text) # => nil
105
+ def selector(method_name)
106
+ METHODS[method_name]&.last
107
+ end
108
+
109
+ # @param selector [Symbol]
110
+ # @return [Boolean]
111
+ # @example
112
+ # built_in_selector?(:css) # => true
113
+ # built_in_selector?(:custom) # => false
114
+ def built_in_selector?(selector)
115
+ BUILT_IN_SELECTORS.include?(selector)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Capybara
6
+ # Shared visibility query checks and offense handling.
7
+ # @api private
8
+ module Visibility
9
+ extend RuboCop::AST::NodePattern::Macros
10
+
11
+ # @!method custom_filters?(node)
12
+ def_node_matcher :custom_filters?, <<~PATTERN
13
+ (hash <{(pair (sym :filter_set) _) (pair !sym _) kwsplat} ...>)
14
+ PATTERN
15
+
16
+ private
17
+
18
+ def supported_visibility_query?(node, options)
19
+ return false if options.braces? || custom_filters?(options)
20
+ return true if QueryMethods.selector(node.method_name) != :selector
21
+
22
+ QueryMethods.built_in_selector?(query_selector(node))
23
+ end
24
+
25
+ def query_selector(node)
26
+ selector = node.first_argument
27
+ return selector.value if selector&.sym_type?
28
+
29
+ positional = node.arguments.reject do |arg|
30
+ arg.type?(:hash, :block_pass)
31
+ end
32
+ return if positional.length > 1 && !selector.str_type?
33
+
34
+ cop_config['DefaultSelector'].to_s.to_sym
35
+ end
36
+
37
+ def check_visibility(pair)
38
+ if pair.value.false_type?
39
+ return add_offense(pair, message: CapybaraHelp::VISIBILITY_FALSE_MESSAGE)
40
+ end
41
+
42
+ add_offense(
43
+ pair, message: CapybaraHelp::VISIBILITY_TRUE_MESSAGE
44
+ ) do |corrector|
45
+ next if block_given? && !yield
46
+
47
+ corrector.replace(pair.value, ':visible')
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Capybara
6
+ # Checks that modal handling methods are called with a block.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # accept_confirm
11
+ # page.accept_prompt(with: 'answer')
12
+ #
13
+ # # good
14
+ # accept_confirm { click_button 'Delete' }
15
+ # page.accept_prompt(with: 'answer', &trigger_modal)
16
+ #
17
+ class ModalMethodWithoutBlock < RuboCop::Cop::Base
18
+ MSG = 'Call `%<method>s` with a block.'
19
+ RESTRICT_ON_SEND = %i[
20
+ accept_alert
21
+ accept_confirm
22
+ accept_prompt
23
+ dismiss_confirm
24
+ dismiss_prompt
25
+ ].freeze
26
+
27
+ def on_send(node)
28
+ return if node.block_literal? || node.block_argument?
29
+ return if node.last_argument&.forwarded_args_type?
30
+
31
+ add_offense(node, message: format(MSG, method: node.method_name))
32
+ end
33
+ alias on_csend on_send
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Capybara
6
+ # Enforces use of `Element#[]` instead of `native.attribute`.
7
+ #
8
+ # Only calls on Capybara finder chains are checked.
9
+ #
10
+ # @safety
11
+ # This cop's autocorrection is unsafe because attribute values may
12
+ # differ between the driver API and Capybara's `Element#[]`.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # find('.item').native.attribute(:id)
17
+ # page.find('.item').native.attribute('aria-label')
18
+ #
19
+ # # good
20
+ # find('.item')[:id]
21
+ # page.find('.item')['aria-label']
22
+ #
23
+ class NativeAttribute < RuboCop::Cop::Base
24
+ extend AutoCorrector
25
+ include CapybaraHelp
26
+
27
+ MSG = 'Use `Element#[]` instead of driver-specific `native.attribute`.'
28
+ RESTRICT_ON_SEND = %i[attribute].freeze
29
+
30
+ # @!method native_attribute(node)
31
+ def_node_matcher :native_attribute, <<~PATTERN
32
+ (call
33
+ $(call #element_finder? :native)
34
+ :attribute
35
+ $({str sym} _))
36
+ PATTERN
37
+
38
+ # @!method element_finder?(node)
39
+ def_node_matcher :element_finder?, <<~PATTERN
40
+ {#capybara_receiver?
41
+ (call #capybara_receiver?
42
+ {:ancestor :find :find_button :find_by_id
43
+ :find_field :find_link :first :sibling} ...)
44
+ (any_block #element_finder? ...)
45
+ (begin #element_finder?)}
46
+ PATTERN
47
+
48
+ def on_send(node)
49
+ native_attribute(node) do |native, attribute|
50
+ next unless native.receiver
51
+
52
+ add_offense(node) do |corrector|
53
+ next if attribute.str_type? && attribute.heredoc?
54
+
55
+ correct_attribute(corrector, node, native, attribute)
56
+ end
57
+ end
58
+ end
59
+ alias on_csend on_send
60
+
61
+ private
62
+
63
+ def correct_attribute(corrector, node, native, attribute)
64
+ range = node.source_range.with(begin_pos: native.loc.dot.begin_pos)
65
+ return if processed_source.comments.any? do |comment|
66
+ range.contains?(comment.source_range)
67
+ end
68
+
69
+ safe_navigation = native.csend_type? && node.csend_type?
70
+ template = safe_navigation ? '&.[](%s)' : '[%s]'
71
+ corrector.replace(range, format(template, attribute.source))
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -22,13 +22,13 @@ module RuboCop
22
22
  # end
23
23
  #
24
24
  # # good
25
- # within '#foo' do
25
+ # within :id, 'foo' do
26
26
  # # ...
27
27
  # end
28
28
  #
29
- class RedundantWithinFind < ::RuboCop::Cop::Base
30
- include CssSelector
29
+ class RedundantWithinFind < RuboCop::Cop::Base
31
30
  extend AutoCorrector
31
+
32
32
  MSG = 'Redundant `within %<method>s(...)` call detected.'
33
33
  RESTRICT_ON_SEND = %i[within].freeze
34
34
  FIND_METHODS = Set.new(%i[find find_by_id]).freeze
@@ -41,8 +41,10 @@ module RuboCop
41
41
 
42
42
  def on_send(node)
43
43
  within_find(node) do |find_node|
44
+ replacement = replaced(find_node)
45
+
44
46
  add_offense(find_node, message: msg(find_node)) do |corrector|
45
- corrector.replace(find_node, replaced(find_node))
47
+ corrector.replace(find_node, replacement) if replacement
46
48
  end
47
49
  end
48
50
  end
@@ -54,24 +56,11 @@ module RuboCop
54
56
  end
55
57
 
56
58
  def replaced(node)
57
- unless node.method?(:find_by_id)
58
- return node.arguments.map(&:source).join(', ')
59
- end
60
-
61
- if node.first_argument.str_type?
62
- build_escaped_selector(node.first_argument, node)
63
- else
64
- node.arguments.map(&:source).join(', ')
65
- .sub(/\A(["'])/, '\1#')
66
- end
67
- end
68
-
69
- def build_escaped_selector(first_arg, node)
70
- quote = first_arg.source[0]
71
- escaped_id = CssSelector.css_escape(first_arg.value, quote)
72
- rest_args = node.arguments.drop(1).map(&:source)
59
+ return unless node.first_argument
60
+ return if node.arguments.any?(&:block_pass_type?)
73
61
 
74
- ["#{quote}##{escaped_id}#{quote}", *rest_args].join(', ')
62
+ args = node.arguments.map(&:source).join(', ')
63
+ node.method?(:find_by_id) ? ":id, #{args}" : args
75
64
  end
76
65
  end
77
66
  end
@@ -25,9 +25,9 @@ module RuboCop
25
25
  # expect(page).to have_current_path('/callback', ignore_query: true)
26
26
  #
27
27
  # # bad (does not support autocorrection when `match` with a variable)
28
- # expect(page).to match(variable)
28
+ # expect(current_path).to match(variable)
29
29
  #
30
- class CurrentPathExpectation < ::RuboCop::Cop::Base
30
+ class CurrentPathExpectation < RuboCop::Cop::Base
31
31
  extend AutoCorrector
32
32
  include RangeHelp
33
33
 
@@ -79,8 +79,11 @@ module RuboCop
79
79
  end
80
80
 
81
81
  regexp_node_matcher(node.parent) do |to_sym, matcher_node, regexp|
82
+ regexp_expr = regexp_node_to_regexp_expr(regexp)
83
+ next unless regexp_expr
84
+
82
85
  rewrite_expectation(corrector, node, to_sym, matcher_node)
83
- convert_regexp_node_to_literal(corrector, matcher_node, regexp)
86
+ corrector.replace(matcher_node.first_argument, regexp_expr)
84
87
  end
85
88
  end
86
89
 
@@ -94,22 +97,19 @@ module RuboCop
94
97
  end
95
98
  corrector.replace(matcher_node.loc.selector, matcher_method)
96
99
  add_argument_parentheses(corrector, matcher_node.first_argument)
97
- add_ignore_query_options(corrector, node, matcher_node)
98
- end
99
-
100
- def convert_regexp_node_to_literal(corrector, matcher_node,
101
- regexp_node)
102
- str_node = matcher_node.first_argument
103
- regexp_expr = regexp_node_to_regexp_expr(regexp_node)
104
- corrector.replace(str_node, regexp_expr)
100
+ add_ignore_query_options(corrector, node)
105
101
  end
106
102
 
107
103
  def regexp_node_to_regexp_expr(regexp_node)
108
104
  if regexp_node.xstr_type?
109
105
  "/\#{`#{regexp_node.value.value}`}/"
110
106
  else
107
+ return if regexp_node.value.match?(/#[{@$]/)
108
+
111
109
  Regexp.new(regexp_node.value).inspect
112
110
  end
111
+ rescue RegexpError
112
+ nil
113
113
  end
114
114
 
115
115
  def add_argument_parentheses(corrector, arg_node)
@@ -129,11 +129,8 @@ module RuboCop
129
129
 
130
130
  # `have_current_path` with no options will include the querystring
131
131
  # while `page.current_path` does not.
132
- # This ensures the option `ignore_query: true` is added
133
- # except when `match` matcher.
134
- def add_ignore_query_options(corrector, node, matcher_node)
135
- return if matcher_node.method?(:match)
136
-
132
+ # This ensures the option `ignore_query: true` is added.
133
+ def add_ignore_query_options(corrector, node)
137
134
  expectation_node = node.parent.last_argument
138
135
  expectation_last_child = expectation_node.children.last
139
136
  corrector.insert_after(expectation_last_child,
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Capybara
6
+ module RSpec
7
+ # Checks expectations on `current_url` that bypass Capybara's waiting.
8
+ #
9
+ # `have_current_path` can match full URLs and waits for navigation.
10
+ # A regexp needs `url: true` so it matches the full URL, not just the
11
+ # path. Autocorrection is unsafe because URL parsing and waiting change
12
+ # comparison behavior.
13
+ # Dynamic `match` patterns and `include`, `start_with`, and `end_with`
14
+ # are reported without autocorrection.
15
+ #
16
+ # @example
17
+ # # bad
18
+ # expect(page.current_url).to eq('https://example.com/login')
19
+ # expect(current_url).to match(%r{/login})
20
+ # expect(current_url).to match('login')
21
+ # expect(page.current_url).to include('/login')
22
+ #
23
+ # # good
24
+ # expect(page).to have_current_path('https://example.com/login')
25
+ # expect(page).to have_current_path(%r{/login}, url: true)
26
+ # expect(page).to have_current_path(/login/, url: true)
27
+ #
28
+ class CurrentUrlExpectation < RuboCop::Cop::Base
29
+ extend AutoCorrector
30
+
31
+ MSG = 'Use `have_current_path` on `page` to wait for the URL.'
32
+ RESTRICT_ON_SEND = %i[expect].freeze
33
+ PATH_MATCHERS = {
34
+ to: 'have_current_path',
35
+ not_to: 'have_no_current_path',
36
+ to_not: 'have_no_current_path'
37
+ }.freeze
38
+
39
+ # @!method url_actual(node)
40
+ def_node_matcher :url_actual, <<~PATTERN
41
+ (send nil? :expect
42
+ $(send {(send nil? :page) nil?} :current_url))
43
+ PATTERN
44
+
45
+ # @!method supported_matcher?(node)
46
+ def_node_matcher :supported_matcher?, <<~PATTERN
47
+ {(send nil? :eq _)
48
+ (send nil? :match _)
49
+ (send nil? {:include :start_with :end_with} (str _))}
50
+ PATTERN
51
+
52
+ # @!method expectation_runner?(node)
53
+ def_node_matcher :expectation_runner?, <<~PATTERN
54
+ (send (send nil? :expect _) {:to :not_to :to_not} _)
55
+ PATTERN
56
+
57
+ def self.autocorrect_incompatible_with
58
+ [Style::TrailingCommaInArguments]
59
+ end
60
+
61
+ def on_send(node)
62
+ actual = url_actual(node)
63
+ return unless actual
64
+
65
+ runner = node.parent
66
+ return unless expectation_runner?(runner)
67
+
68
+ matcher = runner.first_argument
69
+ return unless supported_matcher?(matcher)
70
+
71
+ register_offense(node.loc.selector, actual, runner, matcher)
72
+ end
73
+ alias on_csend on_send
74
+
75
+ private
76
+
77
+ def register_offense(range, actual, runner, matcher)
78
+ regexp = if matcher.method?(:match)
79
+ regexp_source(matcher.first_argument)
80
+ end
81
+ unless correctable_matcher?(matcher, regexp)
82
+ return add_offense(range)
83
+ end
84
+
85
+ add_offense(range) do |corrector|
86
+ autocorrect(corrector, actual, runner, matcher, regexp)
87
+ end
88
+ end
89
+
90
+ def correctable_matcher?(matcher, regexp)
91
+ return true if matcher.method?(:eq)
92
+ return false unless matcher.method?(:match)
93
+
94
+ matcher.first_argument.regexp_type? || regexp
95
+ end
96
+
97
+ def regexp_source(argument)
98
+ return unless argument.str_type?
99
+ return if argument.value.match?(/#[{@$]/)
100
+
101
+ Regexp.new(argument.value).inspect
102
+ rescue RegexpError
103
+ nil
104
+ end
105
+
106
+ def autocorrect(corrector, actual, runner, matcher, regexp)
107
+ corrector.replace(actual, 'page')
108
+ unless runner.method?(:to)
109
+ corrector.replace(runner.loc.selector, 'to')
110
+ end
111
+ corrector.replace(matcher.loc.selector,
112
+ PATH_MATCHERS.fetch(runner.method_name))
113
+ return unless matcher.method?(:match)
114
+
115
+ corrector.replace(matcher.first_argument, regexp) if regexp
116
+ corrector.insert_after(matcher.first_argument, ', url: true')
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end