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

Fix an incorrect autocorrect for Capybara/RedundantWithinFind when escape required css selector #137

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- Add `Capybara/AmbiguousClick` cop and make soft-deprecated `Capybara/ClickLinkOrButtonStyle` cop. If you want to use `EnforcedStyle: strict`, use `Capybara/AmbiguousClick` cop instead. ([@ydah])
- Add new `Capybara/FindAllFirst` cop. ([@ydah])
- Fix an error for `Capybara/RSpec/HaveSelector` when passing no arguments. ([@earlopain])
- Fix an incorrect autocorrect for `Capybara/RedundantWithinFind` when escape required css selector. ([@ydah])

## 2.21.0 (2024-06-08)

Expand Down
45 changes: 45 additions & 0 deletions lib/rubocop/cop/capybara/mixin/css_selector.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'strscan'

module RuboCop
module Cop
module Capybara
Expand Down Expand Up @@ -83,6 +85,49 @@ def multiple_selectors?(selector)
normalize = selector.gsub(/(\\[>,+~]|\(.*\))/, '')
normalize.match?(/[ >,+~]/)
end

# @param selector [String]
# @return [String]
# @example
# css_escape('some-id') # => some-id
# css_escape('some-id.with-priod') # => some-id\.with-priod
# @reference
# https://github.com/mathiasbynens/CSS.escape/blob/master/css.escape.js
def css_escape(selector) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
Copy link
Member

Choose a reason for hiding this comment

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

How do you feel to only add handling for the period in this PR, and leaving the rest for later?

scanner = StringScanner.new(selector)
result = +''

# Special case: if the selector is of length 1 and
# the first character is `-`
if selector.length == 1 && scanner.peek(1) == '-'
return "\\#{selector}"
end

until scanner.eos?
# NULL character (U+0000)
if scanner.scan(/\0/)
result << "\uFFFD"
# Control characters (U+0001 to U+001F, U+007F)
elsif scanner.scan(/[\x01-\x1F\x7F]/)
result << "\\#{scanner.matched.ord.to_s(16)} "
# First character is a digit (U+0030 to U+0039)
elsif scanner.pos.zero? && scanner.scan(/\d/)
result << "\\#{scanner.matched.ord.to_s(16)} "
# Second character is a digit and first is `-`
elsif scanner.pos == 1 && scanner.scan(/\d/) &&
scanner.pre_match == '-'
result << "\\#{scanner.matched.ord.to_s(16)} "
# Alphanumeric characters, `-`, `_`
elsif scanner.scan(/[a-zA-Z0-9\-_]/)
result << scanner.matched
# Any other characters, escape them
elsif scanner.scan(/./)
result << "\\#{scanner.matched}"
end
end

result
end
end
end
end
Expand Down
10 changes: 7 additions & 3 deletions lib/rubocop/cop/capybara/redundant_within_find.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,15 @@ def msg(node)
end

def replaced(node)
replaced = node.arguments.map(&:source).join(', ')
if node.method?(:find_by_id)
replaced.sub(/\A(["'])/, '\1#')
node.first_argument.source.match(/\A(['"])(.*)['"]\z/) do
Copy link
Member

Choose a reason for hiding this comment

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

This won’t work with any other form of string, like triple-quoted, those weird %{} etc ones, won’t work with multi-line?
I suggest creating an issue to keep track of this

Copy link
Member

Choose a reason for hiding this comment

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

Can we use something like string_value, does rubocop-ast provide this?

Copy link
Member

Choose a reason for hiding this comment

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

⚠️This also has a risk of introducing an incorrect match for strings that contain the other quote inside them and being multi-line:

"[id='
boom']"

Doesn’t it?

quote = ::Regexp.last_match(1)
css = ::Regexp.last_match(2)
return ["#{quote}##{CssSelector.css_escape(css)}#{quote}",
*node.arguments[1..].map(&:source)].join(', ')
end
else
replaced
node.arguments.map(&:source).join(', ')
end
end
end
Expand Down
69 changes: 69 additions & 0 deletions spec/rubocop/cop/capybara/mixin/css_selector_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,73 @@
expect(described_class.multiple_selectors?('a.cls\>b')).to be false
end
end

describe 'CssSelector.css_escape' do
context 'when selector is a single hyphen' do
let(:selector) { '-' }

it 'escapes the hyphen character' do
Copy link
Member

Choose a reason for hiding this comment

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

Why? Does it have a special meaning as a CSS selector?

expect(described_class.css_escape(selector)).to eq('\\-')
end
end

context 'when selector contains NULL character' do
let(:selector) { "abc\0def" }
Copy link
Member

Choose a reason for hiding this comment

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

Is this realistic?


it 'replaces NULL character with U+FFFD' do
expect(described_class.css_escape(selector)).to eq('abc�def')
end
end

context 'when selector contains control characters' do
let(:selector) { "abc\x01\x1F\x7Fdef" }

it 'escapes control characters as hexadecimal with a trailing space' do
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need to handle this?

expect(described_class.css_escape(selector))
.to eq('abc\\1 \\1f \\7f def')
end
end

context 'when selector starts with a digit' do
Copy link
Member

Choose a reason for hiding this comment

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

Is this possible? Why would anyone do this?

let(:selector) { '1abc' }

it 'escapes the starting digit as hexadecimal with a trailing space' do
expect(described_class.css_escape(selector)).to eq('\\31 abc')
end
end

context 'when selector starts with a hyphen followed by a digit' do
Copy link
Member

Choose a reason for hiding this comment

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

In CSS, identifiers (including element names, classes, and IDs in selectors) can contain only the characters [a-zA-Z0-9] and ISO 10646 characters U+00A0 and higher, plus the hyphen (-) and the underscore (_); they cannot start with a digit, two hyphens, or a hyphen followed by a digit.

This seems to e an invalid CSS selector case, why should we handle it?

let(:selector) { '-1abc' }

it 'escapes the digit following a hyphen as hexadecimal with a ' \
'trailing space' do
expect(described_class.css_escape(selector)).to eq('-\\31 abc')
end
end

context 'when selector contains alphanumeric characters, hyphen, or ' \
'underscore' do
let(:selector) { 'a-Z_0-9' }

it 'does not escape alphanumeric characters, hyphen, or underscore' do
expect(described_class.css_escape(selector)).to eq('a-Z_0-9')
end
end

context 'when selector contains special characters needing escape' do
let(:selector) { 'a!b@c#d$' }

it 'escapes special characters' do
expect(described_class.css_escape(selector)).to eq('a\\!b\\@c\\#d\\$')
end
end

context 'when selector contains mixed cases' do
let(:selector) { "ab\x00\x7F" }

it 'handles mixed cases appropriately' do
expect(described_class.css_escape(selector)).to eq('ab�\\7f ')
end
end
end
end
13 changes: 13 additions & 0 deletions spec/rubocop/cop/capybara/redundant_within_find_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@
RUBY
end

it 'registers an offense when using `within find_by_id("foo.bar")`' do
expect_offense(<<~RUBY)
within find_by_id('foo.bar') do
^^^^^^^^^^^^^^^^^^^^^ Redundant `within find_by_id(...)` call detected.
end
RUBY

expect_correction(<<~'RUBY')
within '#foo\.bar' do
end
RUBY
end

it 'registers an offense when using `within find_by_id(...)` with ' \
'other argument' do
expect_offense(<<~RUBY)
Expand Down