class RuboCop::Cop::Style::RedundantFormat
‘foo bar’
# good
sprintf(‘%s %s’, ‘foo’, ‘bar’)
format(‘%s %s’, ‘foo’, ‘bar’)
# bad
MESSAGE
# good
sprintf(MESSAGE)
format(MESSAGE)
# bad
’the quick brown fox jumps over the lazy dog.‘
# good
sprintf(’the quick brown fox jumps over the lazy dog.‘)
format(’the quick brown fox jumps over the lazy dog.‘)
# bad
@example
—-
’template’.frozen? # => true
format(‘template’).frozen? # => false
# frozen_string_literal: true
—-
[source,ruby]
this autocorrection is inherently unsafe.
Additionally, since the necessity of ‘dup` cannot be determined automatically,
Consider using `’string’.dup` instead of ‘format(’string’)‘.
`’string’‘, `FrozenError` may occur when calling a destructive method like `String#<<`.
`format` and `sprintf` are never frozen. If `format(’string’)‘ is autocorrected to
This cop’s autocorrection is unsafe because string object returned by
@safety
`%f` format specifiers.
inlined into a string easily. This applies to the ‘%s`, `%d`, `%i`, `%u`, and
Also looks for `format` calls where the arguments are literals that can be
as it can be replaced by the string or constant itself.
Calling `format` with only a single string or constant argument is redundant,
Checks for calls to `Kernel#format` or `Kernel#sprintf` that are redundant.
def all_fields_literal?(string, arguments)
def all_fields_literal?(string, arguments) count = 0 sequences = RuboCop::Cop::Utils::FormatString.new(string).format_sequences return false unless sequences.any? sequences.each do |sequence| next if sequence.percent? hash = arguments.detect(&:hash_type?) next unless (argument = find_argument(sequence, arguments, hash)) next unless matching_argument?(sequence, argument) count += 1 end sequences.size == count end
def argument_value(argument)
def argument_value(argument) argument = argument.children.first if argument.begin_type? if argument.dsym_type? dsym_value(argument) elsif argument.hash_type? hash_value(argument) elsif rational_number?(argument) rational_value(argument) elsif complex_number?(argument) complex_value(argument) elsif argument.respond_to?(:value) argument.value else argument.source end end
def argument_values(arguments)
def argument_values(arguments) arguments.map { |argument| argument_value(argument) } end
def complex_value(complex_node)
def complex_value(complex_node) Complex(complex_node.source) end
def detect_unnecessary_fields(node)
def detect_unnecessary_fields(node) return unless node.first_argument&.str_type? string = node.first_argument.value arguments = node.arguments[1..] return unless string && arguments.any? return if splatted_arguments?(node) register_all_fields_literal(node, string, arguments) end
def dsym_value(dsym_node)
def dsym_value(dsym_node) dsym_node.children.first.source end
def find_argument(sequence, arguments, hash)
def find_argument(sequence, arguments, hash) if hash && (sequence.annotated? || sequence.template?) find_hash_value_node(hash, sequence.name.to_sym).first elsif sequence.arg_number arguments[sequence.arg_number.to_i - 1] else # If the specifier contains `*`, the following arguments will be used # to specify the width and can be ignored. (sequence.arity - 1).times { arguments.shift } arguments.shift end end
def float?(argument)
def float?(argument) numeric?(argument) && Float(argument_value(argument), exception: false) end
def hash_value(hash_node)
def hash_value(hash_node) hash_node.each_pair.with_object({}) do |pair, hash| hash[pair.key.value] = argument_value(pair.value) end end
def integer?(argument)
def integer?(argument) numeric?(argument) && Integer(argument_value(argument), exception: false) end
def matching_argument?(sequence, argument)
def matching_argument?(sequence, argument) # Template specifiers don't give a type, any acceptable literal type is ok. return argument.type?(*ACCEPTABLE_LITERAL_TYPES) if sequence.template? # An argument matches a specifier if it can be easily converted # to that type. case sequence.type when 's' argument.type?(*ACCEPTABLE_LITERAL_TYPES) when 'd', 'i', 'u' integer?(argument) when 'f' float?(argument) else false end end
def message(node, prefer)
def message(node, prefer) format(MSG, prefer: prefer, method_name: node.method_name) end
def numeric?(argument)
def numeric?(argument) argument.type?(:numeric, :str) || rational_number?(argument) || complex_number?(argument) end
def on_send(node)
def on_send(node) format_without_additional_args?(node) do |value| replacement = value.source add_offense(node, message: message(node, replacement)) do |corrector| corrector.replace(node, replacement) end return end detect_unnecessary_fields(node) end
def quote(string, node)
Add correct quotes to the formatted string, preferring retaining the existing
def quote(string, node) str_node = node.first_argument start_delimiter = str_node.loc.begin.source end_delimiter = str_node.loc.end.source # If there is any interpolation, the delimiters need to be changed potentially if node.each_descendant(:dstr, :dsym).any? case start_delimiter when "'" start_delimiter = end_delimiter = '"' when /\A%q(.)/ start_delimiter = "%Q#{Regexp.last_match[1]}" end end "#{start_delimiter}#{string}#{end_delimiter}" end
def rational_value(rational_node)
def rational_value(rational_node) rational_node.source.to_r end
def register_all_fields_literal(node, string, arguments)
def register_all_fields_literal(node, string, arguments) return unless all_fields_literal?(string, arguments.dup) format_arguments = argument_values(arguments) begin formatted_string = format(string, *format_arguments) rescue ArgumentError return end replacement = quote(formatted_string, node) add_offense(node, message: message(node, replacement)) do |corrector| corrector.replace(node, replacement) end end