Skip to content

Commit

Permalink
Add Style/RedundantRegexpEscape cop (#7908)
Browse files Browse the repository at this point in the history
  • Loading branch information
owst committed May 31, 2020
1 parent 43e07e0 commit 3d064b4
Show file tree
Hide file tree
Showing 16 changed files with 472 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@

* [#6289](https://github.com/rubocop-hq/rubocop/issues/6289): Add new `CheckDefinitionPathHierarchy` option for `Naming/FileName`. ([@jschneid][])
* [#8069](https://github.com/rubocop-hq/rubocop/issues/8069): New option for `expect_offense` to help format offense templates. ([@marcandre][])
* [#7908](https://github.com/rubocop-hq/rubocop/pull/7908): Add new `Style/RedundantRegexpEscape` cop. ([@owst][])

### Bug fixes

Expand Down
5 changes: 5 additions & 0 deletions config/default.yml
Expand Up @@ -3607,6 +3607,11 @@ Style/RedundantPercentQ:
Enabled: true
VersionAdded: '0.76'

Style/RedundantRegexpEscape:
Description: 'Checks for redundant escapes in Regexps.'
Enabled: pending
VersionAdded: '0.85'

Style/RedundantReturn:
Description: "Don't use return where it's not required."
StyleGuide: '#no-explicit-return'
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/cops.adoc
Expand Up @@ -437,6 +437,7 @@ In the following section you find all available cops:
* xref:cops_style.adoc#styleredundantinterpolation[Style/RedundantInterpolation]
* xref:cops_style.adoc#styleredundantparentheses[Style/RedundantParentheses]
* xref:cops_style.adoc#styleredundantpercentq[Style/RedundantPercentQ]
* xref:cops_style.adoc#styleredundantregexpescape[Style/RedundantRegexpEscape]
* xref:cops_style.adoc#styleredundantreturn[Style/RedundantReturn]
* xref:cops_style.adoc#styleredundantself[Style/RedundantSelf]
* xref:cops_style.adoc#styleredundantsort[Style/RedundantSort]
Expand Down
43 changes: 43 additions & 0 deletions docs/modules/ROOT/pages/cops_style.adoc
Expand Up @@ -7116,6 +7116,49 @@ question = '"What did you say?"'

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

== Style/RedundantRegexpEscape

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

| Pending
| 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 @@ -119,7 +119,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
ALLOWED_WITHIN_CHAR_CLASS_METACHAR_ESCAPES = '-'.chars.freeze
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
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

0 comments on commit 3d064b4

Please sign in to comment.