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 Style/RedundantRegexpEscape cop #7908

Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -43,6 +43,7 @@
* [#7951](https://github.com/rubocop-hq/rubocop/pull/7951): Include `rakefile` file by default. ([@jethrodaniel][])
* [#7921](https://github.com/rubocop-hq/rubocop/pull/7921): Add new `Style/SlicingWithRange` cop. ([@zverok][])
* [#7895](https://github.com/rubocop-hq/rubocop/pull/7895): Include `.simplecov` file by default. ([@robotdana][])
* [#7908](https://github.com/rubocop-hq/rubocop/pull/7908): Add new `Style/RedundantRegexpEscape` cop. ([@owst][])
* [#7916](https://github.com/rubocop-hq/rubocop/pull/7916): Support autocorrection for `Lint/AmbiguousRegexpLiteral`. ([@koic][])
* [#7917](https://github.com/rubocop-hq/rubocop/pull/7917): Support autocorrection for `Lint/UselessAccessModifier`. ([@koic][])
* [#595](https://github.com/rubocop-hq/rubocop/issues/595): Add ERB pre-processing for configuration files. ([@jonas054][])
Expand Down
5 changes: 5 additions & 0 deletions config/default.yml
Expand Up @@ -3610,6 +3610,11 @@ Style/RedundantPercentQ:
Enabled: true
VersionAdded: '0.76'

Style/RedundantRegexpEscape:
Description: 'Checks for redundant escapes in Regexps.'
Enabled: true
VersionAdded: '0.85'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if the next version will be 0.85, or if it will be 1.0. @bbatsov knows.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems it will be 0.85. Too much development happening recently. I want to give @marcandre time to finish the things he has in progress.


Style/RedundantReturn:
Description: "Don't use return where it's not required."
StyleGuide: '#no-explicit-return'
Expand Down
43 changes: 43 additions & 0 deletions docs/modules/ROOT/pages/cops_style.adoc
Expand Up @@ -7117,6 +7117,49 @@ question = '"What did you say?"'

* https://rubystyle.guide#percent-q

== Style/RedundantRegexpEscape

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

| Enabled
| Yes
| Yes
| 0.85
| -
|===

This cop checks for redundant escapes inside Regexp literals.

=== Examples

[source,ruby]
----
# bad
%r{foo\/bar}

# good
%r{foo/bar}

# good
/foo\/bar/

# good
%r/foo\/bar/

# bad
/a\-b/

# good
/a-b/

# bad
/[\+\-]\d/

# good
/[+\-]\d/
----

== Style/RedundantReturn

|===
Expand Down
1 change: 1 addition & 0 deletions lib/rubocop.rb
Expand Up @@ -475,6 +475,7 @@
require_relative 'rubocop/cop/style/redundant_interpolation'
require_relative 'rubocop/cop/style/redundant_parentheses'
require_relative 'rubocop/cop/style/redundant_percent_q'
require_relative 'rubocop/cop/style/redundant_regexp_escape'
require_relative 'rubocop/cop/style/redundant_return'
require_relative 'rubocop/cop/style/redundant_self'
require_relative 'rubocop/cop/style/redundant_sort'
Expand Down
2 changes: 1 addition & 1 deletion lib/rubocop/cop/layout/first_array_element_line_break.rb
Expand Up @@ -37,7 +37,7 @@ def autocorrect(node)

def assignment_on_same_line?(node)
source = node.source_range.source_line[0...node.loc.column]
source =~ /\s*\=\s*$/
source =~ /\s*=\s*$/
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/rubocop/cop/layout/space_around_keyword.rb
Expand Up @@ -186,7 +186,7 @@ def space_before_missing?(range)
pos = range.begin_pos - 1
return false if pos.negative?

range.source_buffer.source[pos] !~ /[\s\(\|\{\[;,\*\=]/
range.source_buffer.source[pos] !~ /[\s(|{\[;,*=]/
end

def space_after_missing?(range)
Expand All @@ -198,7 +198,7 @@ def space_after_missing?(range)
return false if accept_namespace_operator?(range) &&
namespace_operator?(range, pos)

char !~ /[\s;,#\\\)\}\]\.]/
char !~ /[\s;,#\\)}\].]/
end

def accepted_opening_delimiter?(range, char)
Expand Down
2 changes: 1 addition & 1 deletion lib/rubocop/cop/naming/file_name.rb
Expand Up @@ -104,7 +104,7 @@ def allowed_acronyms

def filename_good?(basename)
basename = basename.sub(/^\./, '')
basename = basename.sub(/\.[^\.]+$/, '')
basename = basename.sub(/\.[^.]+$/, '')
# special handling for Action Pack Variants file names like
# some_file.xlsx+mobile.axlsx
basename = basename.sub('+', '_')
Expand Down
2 changes: 1 addition & 1 deletion lib/rubocop/cop/style/multiline_memoization.rb
Expand Up @@ -82,7 +82,7 @@ def keyword_begin_str(node, node_buf)
end

def keyword_end_str(node, node_buf)
if /[^\s\)]/.match?(node_buf.source_line(node.loc.end.line))
if /[^\s)]/.match?(node_buf.source_line(node.loc.end.line))
"\n" + (' ' * node.loc.column) + 'end'
else
'end'
Expand Down
118 changes: 118 additions & 0 deletions lib/rubocop/cop/style/redundant_regexp_escape.rb
@@ -0,0 +1,118 @@
# frozen_string_literal: true

module RuboCop
module Cop
module Style
# This cop checks for redundant escapes inside Regexp literals.
#
# @example
# # bad
# %r{foo\/bar}
#
# # good
# %r{foo/bar}
#
# # good
# /foo\/bar/
#
# # good
# %r/foo\/bar/
#
# # bad
# /a\-b/
#
# # good
# /a-b/
#
# # bad
# /[\+\-]\d/
#
# # good
# /[+\-]\d/
class RedundantRegexpEscape < Cop
include RangeHelp

MSG_REDUNDANT_ESCAPE = 'Redundant escape inside regexp literal'

ALLOWED_ALWAYS_ESCAPES = ' []^\\#'.chars.freeze
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why the space character is always allowed to escape. Is it when you use the x flag?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly. I could add another explicit message that said "Unnecessary escape of space, when not using free-spacing (x) mode."?

ALLOWED_WITHIN_CHAR_CLASS_METACHAR_ESCAPES = '-'.chars.freeze
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can always put a dash at the beginning or end of a character class and not have to escape it, right? It would require more logic in the cop, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's true - it's similar to how ^ only needs to be escaped if it is the first character of a character class.

My two reasons for not flagging/removing such escapes is that it would involve more logic as you say, and make the resulting character classes slightly more "fragile" to changes over time - either by adding another character or by reordering the elements of the character class, e.g.:

[a-] => [a-b]
[-a] => [b-a]
[ab-] => [a-b]
[a^] => [^a]

would all change the meaning of the character class. The refactorings may not be common, but I think I fall down on allowing escapes of ^ and - even though they are technically redundant. What do you think? I can take a look at the implementation would be if you think it's worth flagging such cases.

ALLOWED_OUTSIDE_CHAR_CLASS_METACHAR_ESCAPES = '.*+?{}()|$'.chars.freeze

def on_regexp(node)
each_escape(node) do |char, index, within_character_class|
next if allowed_escape?(node, char, within_character_class)

add_offense(
node,
location: escape_range_at_index(node, index),
message: MSG_REDUNDANT_ESCAPE
)
end
end

def autocorrect(node)
lambda do |corrector|
each_escape(node) do |char, index, within_character_class|
next if allowed_escape?(node, char, within_character_class)

corrector.remove_leading(escape_range_at_index(node, index), 1)
end
end
end

private

def slash_literal?(node)
['/', '%r/'].include?(node.loc.begin.source)
end

def allowed_escape?(node, char, within_character_class)
# Strictly speaking a few single-letter metachars are currently
# unneccessary to "escape", e.g. g, i, E, F, but enumerating them is
# rather difficult, and their behaviour could change over time with
# different versions of Ruby so that e.g. /\g/ != /g/
return true if /[[:alnum:]]/.match?(char)
return true if ALLOWED_ALWAYS_ESCAPES.include?(char)

if char == '/'
slash_literal?(node)
elsif within_character_class
ALLOWED_WITHIN_CHAR_CLASS_METACHAR_ESCAPES.include?(char)
else
ALLOWED_OUTSIDE_CHAR_CLASS_METACHAR_ESCAPES.include?(char)
end
end

def each_escape(node)
pattern_source(node).each_char.with_index.reduce(
[nil, false]
) do |(previous, within_character_class), (current, index)|
if previous == '\\'
yield [current, index - 1, within_character_class]

[nil, within_character_class]
elsif previous == '[' && current != ':'
[current, true]
elsif previous != ':' && current == ']'
[current, false]
else
[current, within_character_class]
end
end
end

def escape_range_at_index(node, index)
regexp_begin = node.loc.begin.end_pos

start = regexp_begin + index

range_between(start, start + 2)
end

def pattern_source(node)
node.children.reject(&:regopt_type?).map(&:source).join
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/rubocop/cop/style/symbol_array.rb
Expand Up @@ -106,7 +106,7 @@ def symbol_without_quote?(string)
# method name
string =~ /\A[a-zA-Z_]\w*[!?]?\z/ ||
# instance / class variable
string =~ /\A\@\@?[a-zA-Z_]\w*\z/ ||
string =~ /\A@@?[a-zA-Z_]\w*\z/ ||
# global variable
string =~ /\A\$[1-9]\d*\z/ ||
string =~ /\A\$[a-zA-Z_]\w*\z/ ||
Expand Down
2 changes: 1 addition & 1 deletion lib/rubocop/magic_comment.rb
Expand Up @@ -133,7 +133,7 @@ def tokens
# @see https://www.gnu.org/software/emacs/manual/html_node/emacs/Specify-Coding.html
# @see https://git.io/vMCXh Emacs handling in Ruby's parse.y
class EmacsComment < EditorComment
FORMAT = /\-\*\-(.+)\-\*\-/.freeze
FORMAT = /-\*-(.+)-\*-/.freeze
SEPARATOR = ';'
OPERATOR = ':'

Expand Down
1 change: 1 addition & 0 deletions manual/cops.md
Expand Up @@ -435,6 +435,7 @@ In the following section you find all available cops:
* [Style/RedundantInterpolation](cops_style.md#styleredundantinterpolation)
* [Style/RedundantParentheses](cops_style.md#styleredundantparentheses)
* [Style/RedundantPercentQ](cops_style.md#styleredundantpercentq)
* [Style/RedundantRegexpEscape](cops_style.md#styleredundantregexpescape)
* [Style/RedundantReturn](cops_style.md#styleredundantreturn)
* [Style/RedundantSelf](cops_style.md#styleredundantself)
* [Style/RedundantSort](cops_style.md#styleredundantsort)
Expand Down
36 changes: 36 additions & 0 deletions manual/cops_style.md
Expand Up @@ -5741,6 +5741,42 @@ question = '"What did you say?"'

* [https://rubystyle.guide#percent-q](https://rubystyle.guide#percent-q)

## Style/RedundantRegexpEscape

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
--- | --- | --- | --- | ---
Enabled | Yes | Yes | 0.83 | -
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the file needs to be generated again, looking at the "version added".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, yes, good catch, I missed this after I merged master and bumped the VersionAdded you noted above - I'll regenerate now and can change again when @bbatsov advises about the next version number


This cop checks for redundant escapes inside Regexp literals.

### Examples

```ruby
# bad
%r{foo\/bar}

# good
%r{foo/bar}

# good
/foo\/bar/

# good
%r/foo\/bar/

# bad
/a\-b/

# good
/a-b/

# bad
/[\+\-]\d/

# good
/[+\-]\d/
```

## Style/RedundantReturn

Enabled by default | Safe | Supports autocorrection | VersionAdded | VersionChanged
Expand Down
6 changes: 3 additions & 3 deletions spec/project_spec.rb
Expand Up @@ -150,7 +150,7 @@
describe 'link to related issue' do
let(:issues) do
entries.map do |entry|
entry.match(/\[(?<number>[#\d]+)\]\((?<url>[^\)]+)\)/)
entry.match(/\[(?<number>[#\d]+)\]\((?<url>[^)]+)\)/)
end.compact
end

Expand Down Expand Up @@ -191,7 +191,7 @@
entry
.gsub(/`[^`]+`/, '``')
.sub(/^\*\s*(?:\[.+?\):\s*)?/, '')
.sub(/\s*\([^\)]+\)$/, '')
.sub(/\s*\([^)]+\)$/, '')
end
end

Expand All @@ -202,7 +202,7 @@
end

it 'ends with a punctuation' do
expect(bodies).to all(match(/[\.\!]$/))
expect(bodies).to all(match(/[.!]$/))
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions spec/rubocop/cli/cli_options_spec.rb
Expand Up @@ -1005,8 +1005,8 @@ def full_description_of_cop(cop)
' Description: Consistent indentation either with tabs only or spaces only.',
/^ StyleGuide: ('|")#spaces-indentation('|")$/,
' Enabled: true',
/^ VersionAdded: '[0-9\.]+'$/,
/^ VersionChanged: '[0-9\.]+'$/,
/^ VersionAdded: '[0-9.]+'$/,
/^ VersionChanged: '[0-9.]+'$/,
' IndentationWidth:'].join("\n")
)
end
Expand Down