Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new Lint/OutOfRangeRefInRegexp cop #7755 #8407

Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -21,6 +21,7 @@
* [#8417](https://github.com/rubocop-hq/rubocop/pull/8417): Add new `Style/GlobalStdStream` cop. ([@fatkodima][])
* [#7949](https://github.com/rubocop-hq/rubocop/issues/7949): Add new `Style/SingleArgumentDig` cop. ([@volfgox][])
* [#8341](https://github.com/rubocop-hq/rubocop/pull/8341): Add new `Lint/EmptyConditionalBody` cop. ([@fatkodima][])
* [#7755](https://github.com/rubocop-hq/rubocop/issues/7755): Add new `Lint/OutOfRangeRegexpRef` cop. ([@sonalinavlakhe][])

### Bug fixes

Expand Down Expand Up @@ -4750,3 +4751,4 @@
[@iamravitejag]: https://github.com/iamravitejag
[@volfgox]: https://github.com/volfgox
[@dsavochkin]: https://github.com/dmytro-savochkin
[@sonalinavlakhe]: https://github.com/sonalinavlakhe
6 changes: 6 additions & 0 deletions config/default.yml
Expand Up @@ -1640,6 +1640,12 @@ Lint/OrderedMagicComments:
Enabled: true
VersionAdded: '0.53'

Lint/OutOfRangeRegexpRef:
Description: 'Checks for out of range reference for Regexp because it always returns nil.'
Enabled: pending
Safe: false
VersionAdded: '0.89'

Lint/ParenthesesAsGroupedExpression:
Description: >-
Checks for method calls with a space before the opening
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/cops.adoc
Expand Up @@ -230,6 +230,7 @@ In the following section you find all available cops:
* xref:cops_lint.adoc#lintnonlocalexitfromiterator[Lint/NonLocalExitFromIterator]
* xref:cops_lint.adoc#lintnumberconversion[Lint/NumberConversion]
* xref:cops_lint.adoc#lintorderedmagiccomments[Lint/OrderedMagicComments]
* xref:cops_lint.adoc#lintoutofrangeregexpref[Lint/OutOfRangeRegexpRef]
* xref:cops_lint.adoc#lintparenthesesasgroupedexpression[Lint/ParenthesesAsGroupedExpression]
* xref:cops_lint.adoc#lintpercentstringarray[Lint/PercentStringArray]
* xref:cops_lint.adoc#lintpercentsymbolarray[Lint/PercentSymbolArray]
Expand Down
30 changes: 30 additions & 0 deletions docs/modules/ROOT/pages/cops_lint.adoc
Expand Up @@ -2356,6 +2356,36 @@ p [''.frozen?, ''.encoding] #=> [true, #<Encoding:US-ASCII>]
p [''.frozen?, ''.encoding] #=> [true, #<Encoding:US-ASCII>]
----

== Lint/OutOfRangeRegexpRef

|===
| Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged

| Pending
| No
| No
| 0.89
| -
|===

This cops looks for references of Regexp captures that are out of range
and thus always returns nil.

=== Examples

[source,ruby]
----
/(foo)bar/ =~ 'foobar'

# bad - always returns nil

puts $2 # => nil

# good

puts $1 # => foo
----

== Lint/ParenthesesAsGroupedExpression

|===
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop.rb
Expand Up @@ -288,6 +288,7 @@
require_relative 'rubocop/cop/lint/non_local_exit_from_iterator'
require_relative 'rubocop/cop/lint/number_conversion'
require_relative 'rubocop/cop/lint/ordered_magic_comments'
require_relative 'rubocop/cop/lint/out_of_range_regexp_ref'
require_relative 'rubocop/cop/lint/parentheses_as_grouped_expression'
require_relative 'rubocop/cop/lint/percent_string_array'
require_relative 'rubocop/cop/lint/percent_symbol_array'
Expand Down
61 changes: 61 additions & 0 deletions lib/rubocop/cop/lint/out_of_range_regexp_ref.rb
@@ -0,0 +1,61 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Lint
# This cops looks for references of Regexp captures that are out of range
# and thus always returns nil.
#
# @example
#
# /(foo)bar/ =~ 'foobar'
#
# # bad - always returns nil
#
# puts $2 # => nil
#
# # good
#
# puts $1 # => foo
#
class OutOfRangeRegexpRef < Base
MSG = 'Do not use out of range reference for the Regexp.'

def on_new_investigation
@valid_ref = 0
end

def on_regexp(node)
@valid_ref = nil
return if contain_non_literal?(node)

tree = Regexp::Parser.parse(node.content)
@valid_ref = regexp_captures(tree)
end

def on_nth_ref(node)
backref, = *node
return if @valid_ref.nil?

add_offense(node) if backref > @valid_ref
end

private

def contain_non_literal?(node)
node.children.size != 2 || !node.children.first.str_type?
end

def regexp_captures(tree)
named_capture = numbered_capture = 0
tree.each_expression do |e|
if e.type?(:group)
e.respond_to?(:name) ? named_capture += 1 : numbered_capture += 1
end
end
named_capture.positive? ? named_capture : numbered_capture
end
end
end
end
end
81 changes: 81 additions & 0 deletions spec/rubocop/cop/lint/out_of_range_regexp_ref_spec.rb
@@ -0,0 +1,81 @@
# frozen_string_literal: true

RSpec.describe RuboCop::Cop::Lint::OutOfRangeRegexpRef do
subject(:cop) { described_class.new(config) }

let(:config) { RuboCop::Config.new }

it 'registers an offense when references are used before any Regexp' do
expect_offense(<<~RUBY)
puts $3
^^ Do not use out of range reference for the Regexp.
RUBY
end

it 'registers an offense when out of range references are used for named captures' do
expect_offense(<<~RUBY)
/(?<foo>FOO)(?<bar>BAR)/ =~ "FOOBAR"
puts $3
^^ Do not use out of range reference for the Regexp.
RUBY
end

it 'registers an offense when out of range references are used for numbered captures' do
expect_offense(<<~RUBY)
/(foo)(bar)/ =~ "foobar"
puts $3
^^ Do not use out of range reference for the Regexp.
RUBY
end

it 'registers an offense when out of range references are used for mix of numbered and named captures' do
expect_offense(<<~RUBY)
/(?<foo>FOO)(BAR)/ =~ "FOOBAR"
puts $2
^^ Do not use out of range reference for the Regexp.
RUBY
end

it 'registers an offense when out of range references are used for non captures' do
expect_offense(<<~RUBY)
/bar/ =~ 'foo'
puts $1
^^ Do not use out of range reference for the Regexp.
RUBY
end

it 'does not register offense to a regexp with valid references for named captures' do
expect_no_offenses(<<~RUBY)
/(?<foo>FOO)(?<bar>BAR)/ =~ "FOOBAR"
puts $1
puts $2
RUBY
end

it 'does not register offense to a regexp with valid references for numbered captures' do
expect_no_offenses(<<~RUBY)
/(foo)(bar)/ =~ "foobar"
puts $1
puts $2
RUBY
end

it 'does not register offense to a regexp with valid references for a mix named and numbered captures' do
expect_no_offenses(<<~RUBY)
/(?<foo>FOO)(BAR)/ =~ "FOOBAR"
puts $1
RUBY
end

# RuboCop does not know a value of variables that it will contain in the regexp literal.
# For example, `/(?<foo>#{var}*)` is interpreted as `/(?<foo>*)`.
# So it does not offense when variables are used in regexp literals.
it 'does not register an offence Regexp containing non literal' do
expect_no_offenses(<<~'RUBY')
var = '(\d+)'
/(?<foo>#{var}*)/ =~ "12"
puts $1
puts $2
RUBY
end
end