diff --git a/changelog/change_add_strnodepercent_literal_to_simplify.md b/changelog/change_add_strnodepercent_literal_to_simplify.md new file mode 100644 index 000000000..424e40dd6 --- /dev/null +++ b/changelog/change_add_strnodepercent_literal_to_simplify.md @@ -0,0 +1 @@ +* [#343](https://github.com/rubocop/rubocop-ast/pull/343): Add `StrNode#single_quoted?`, `StrNode#double_quoted?` and `StrNode#percent_literal?` to simplify checking for string delimiters. ([@dvandersluis][]) diff --git a/lib/rubocop/ast/node/str_node.rb b/lib/rubocop/ast/node/str_node.rb index 6e17bf1e3..a5d95eb74 100644 --- a/lib/rubocop/ast/node/str_node.rb +++ b/lib/rubocop/ast/node/str_node.rb @@ -8,6 +8,20 @@ module AST class StrNode < Node include BasicLiteralNode + PERCENT_LITERAL_TYPES = { + :% => /\A%(?=[^a-zA-Z])/, + :q => /\A%q/, + :Q => /\A%Q/ + }.freeze + + def single_quoted? + loc_is?(:begin, "'") + end + + def double_quoted? + loc_is?(:begin, '"') + end + def character_literal? loc_is?(:begin, '?') end @@ -15,6 +29,28 @@ def character_literal? def heredoc? loc.is_a?(Parser::Source::Map::Heredoc) end + + # Checks whether the string literal is delimited by percent brackets. + # + # @overload percent_literal? + # Check for any string percent literal. + # + # @overload percent_literal?(type) + # Check for a string percent literal of type `type`. + # + # @param type [Symbol] an optional percent literal type + # + # @return [Boolean] whether the string is enclosed in percent brackets + def percent_literal?(type = nil) + opening_delimiter = loc.begin if loc.respond_to?(:begin) + return false unless opening_delimiter + + if type + opening_delimiter.source.match?(PERCENT_LITERAL_TYPES.fetch(type)) + else + opening_delimiter.source.start_with?('%') + end + end end end end diff --git a/spec/rubocop/ast/dstr_node_spec.rb b/spec/rubocop/ast/dstr_node_spec.rb index 156ced2bc..4af1ba1a4 100644 --- a/spec/rubocop/ast/dstr_node_spec.rb +++ b/spec/rubocop/ast/dstr_node_spec.rb @@ -35,4 +35,73 @@ it { is_expected.to eq('foo bar baz') } end end + + describe '#single_quoted?' do + context 'with a double-quoted string' do + let(:source) { '"#{foo}"' } + + it { is_expected.not_to be_single_quoted } + end + + context 'with a %() delimited string' do + let(:source) { '%(#{foo})' } + + it { is_expected.not_to be_single_quoted } + end + + context 'with a %Q() delimited string' do + let(:source) { '%Q(#{foo})' } + + it { is_expected.not_to be_single_quoted } + end + end + + describe '#double_quoted?' do + context 'with a double-quoted string' do + let(:source) { '"#{foo}"' } + + it { is_expected.to be_double_quoted } + end + + context 'with a %() delimited string' do + let(:source) { '%(#{foo})' } + + it { is_expected.not_to be_double_quoted } + end + + context 'with a %Q() delimited string' do + let(:source) { '%Q(#{foo})' } + + it { is_expected.not_to be_double_quoted } + end + end + + describe '#percent_literal?' do + context 'with a quoted string' do + let(:source) { '"#{foo}"' } + + it { is_expected.not_to be_percent_literal } + it { is_expected.not_to be_percent_literal(:%) } + it { is_expected.not_to be_percent_literal(:q) } + it { is_expected.not_to be_percent_literal(:Q) } + end + + context 'with a %() delimited string' do + let(:source) { '%(#{foo})' } + + it { is_expected.to be_percent_literal } + it { is_expected.to be_percent_literal(:%) } + it { is_expected.not_to be_percent_literal(:q) } + it { is_expected.not_to be_percent_literal(:Q) } + end + + context 'with a %Q() delimited string' do + let(:source) { '%Q(#{foo})' } + + it { is_expected.to be_percent_literal } + it { is_expected.not_to be_percent_literal(:%) } + it { is_expected.not_to be_percent_literal(:q) } + it { is_expected.to be_percent_literal(:Q) } + end + end end diff --git a/spec/rubocop/ast/str_node_spec.rb b/spec/rubocop/ast/str_node_spec.rb index 9be2ff23a..0c437191a 100644 --- a/spec/rubocop/ast/str_node_spec.rb +++ b/spec/rubocop/ast/str_node_spec.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true RSpec.describe RuboCop::AST::StrNode do - subject(:str_node) { parse_source(source).ast } + subject(:str_node) { parsed_source.ast } + + let(:parsed_source) { parse_source(source) } describe '.new' do context 'with a normal string' do @@ -30,6 +32,98 @@ end end + describe '#single_quoted?' do + context 'with a single-quoted string' do + let(:source) { "'foo'" } + + it { is_expected.to be_single_quoted } + end + + context 'with a double-quoted string' do + let(:source) { '"foo"' } + + it { is_expected.not_to be_single_quoted } + end + + context 'with a %() delimited string' do + let(:source) { '%(foo)' } + + it { is_expected.not_to be_single_quoted } + end + + context 'with a %q() delimited string' do + let(:source) { '%q(foo)' } + + it { is_expected.not_to be_single_quoted } + end + + context 'with a %Q() delimited string' do + let(:source) { '%Q(foo)' } + + it { is_expected.not_to be_single_quoted } + end + + context 'with a character literal' do + let(:source) { '?x' } + + it { is_expected.not_to be_single_quoted } + end + + context 'with an undelimited string within another node' do + subject(:str_node) { parsed_source.ast.child_nodes.first } + + let(:source) { '/string/' } + + it { is_expected.not_to be_single_quoted } + end + end + + describe '#double_quoted?' do + context 'with a single-quoted string' do + let(:source) { "'foo'" } + + it { is_expected.not_to be_double_quoted } + end + + context 'with a double-quoted string' do + let(:source) { '"foo"' } + + it { is_expected.to be_double_quoted } + end + + context 'with a %() delimited string' do + let(:source) { '%(foo)' } + + it { is_expected.not_to be_double_quoted } + end + + context 'with a %q() delimited string' do + let(:source) { '%q(foo)' } + + it { is_expected.not_to be_double_quoted } + end + + context 'with a %Q() delimited string' do + let(:source) { '%Q(foo)' } + + it { is_expected.not_to be_double_quoted } + end + + context 'with a character literal' do + let(:source) { '?x' } + + it { is_expected.not_to be_double_quoted } + end + + context 'with an undelimited string within another node' do + subject(:str_node) { parsed_source.ast.child_nodes.first } + + let(:source) { '/string/' } + + it { is_expected.not_to be_single_quoted } + end + end + describe '#character_literal?' do context 'with a character literal' do let(:source) { '?\n' } @@ -83,4 +177,81 @@ it { is_expected.to be_heredoc } end end + + describe '#percent_literal?' do + context 'with a single-quoted string' do + let(:source) { "'foo'" } + + it { is_expected.not_to be_percent_literal } + it { is_expected.not_to be_percent_literal(:%) } + it { is_expected.not_to be_percent_literal(:q) } + it { is_expected.not_to be_percent_literal(:Q) } + end + + context 'with a double-quoted string' do + let(:source) { '"foo"' } + + it { is_expected.not_to be_percent_literal } + it { is_expected.not_to be_percent_literal(:%) } + it { is_expected.not_to be_percent_literal(:q) } + it { is_expected.not_to be_percent_literal(:Q) } + end + + context 'with a %() delimited string' do + let(:source) { '%(foo)' } + + it { is_expected.to be_percent_literal } + it { is_expected.to be_percent_literal(:%) } + it { is_expected.not_to be_percent_literal(:q) } + it { is_expected.not_to be_percent_literal(:Q) } + end + + context 'with a %q() delimited string' do + let(:source) { '%q(foo)' } + + it { is_expected.to be_percent_literal } + it { is_expected.not_to be_percent_literal(:%) } + it { is_expected.to be_percent_literal(:q) } + it { is_expected.not_to be_percent_literal(:Q) } + end + + context 'with a %Q() delimited string' do + let(:source) { '%Q(foo)' } + + it { is_expected.to be_percent_literal } + it { is_expected.not_to be_percent_literal(:%) } + it { is_expected.not_to be_percent_literal(:q) } + it { is_expected.to be_percent_literal(:Q) } + end + + context 'with a character literal?' do + let(:source) { '?x' } + + it { is_expected.not_to be_percent_literal } + it { is_expected.not_to be_percent_literal(:%) } + it { is_expected.not_to be_percent_literal(:q) } + it { is_expected.not_to be_percent_literal(:Q) } + end + + context 'with an undelimited string within another node' do + subject(:str_node) { parsed_source.ast.child_nodes.first } + + let(:source) { '/string/' } + + it { is_expected.not_to be_percent_literal } + it { is_expected.not_to be_percent_literal(:%) } + it { is_expected.not_to be_percent_literal(:q) } + it { is_expected.not_to be_percent_literal(:Q) } + end + + context 'when given an invalid type' do + subject { str_node.percent_literal?(:x) } + + let(:source) { '%q(foo)' } + + it 'raises a KeyError' do + expect { str_node.percent_literal?(:x) }.to raise_error(KeyError, 'key not found: :x') + end + end + end end