123456789_123456789_123456789_123456789_123456789_

Class: RuboCop::Cop::Style::RedundantFormat

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, ::RuboCop::Cop::AutoCorrector, ::RuboCop::Cop::Base, ::RuboCop::ExcludeLimit, NodePattern::Macros, RuboCop::AST::Sexp
Instance Chain:
Inherits: RuboCop::Cop::Base
Defined in: lib/rubocop/cop/style/redundant_format.rb

Overview

Checks for calls to Kernel#format or Kernel#sprintf that are redundant.

Calling format with only a single string argument is redundant, as it can be replaced by the string itself.

Also looks for format calls where the arguments are literals that can be inlined into a string easily. This applies to the %s, %d, %i, %u, and %f format specifiers.

Examples:

# bad
format('the quick brown fox jumps over the lazy dog.')
sprintf('the quick brown fox jumps over the lazy dog.')

# good
'the quick brown fox jumps over the lazy dog.'

# bad
format('%s %s', 'foo', 'bar')
sprintf('%s %s', 'foo', 'bar')

# good
'foo bar'

Cop Safety Information:

  • This cop’s autocorrection is unsafe because string object returned by format and sprintf are never frozen. If format('string') is autocorrected to 'string', FrozenError may occur when calling a destructive method like String#<<. Consider using 'string'.dup instead of format('string'). Additionally, since the necessity of dup cannot be determined automatically, this autocorrection is inherently unsafe.

    # frozen_string_literal: true
    
    format('template').frozen? # => false
    'template'.frozen?         # => true

Constant Summary

::RuboCop::Cop::Base - Inherited

EMPTY_OFFENSES, RESTRICT_ON_SEND

Class Attribute Summary

::RuboCop::Cop::AutoCorrector - Extended

::RuboCop::Cop::Base - Inherited

.gem_requirements, .lint?,
.support_autocorrect?

Returns if class supports autocorrect.

.support_multiple_source?

Override if your cop should be called repeatedly for multiple investigations Between calls to on_new_investigation and on_investigation_end, the result of processed_source will remain constant.

Class Method Summary

::RuboCop::Cop::Base - Inherited

.autocorrect_incompatible_with

List of cops that should not try to autocorrect at the same time as this cop.

.badge

Naming.

.callbacks_needed, .cop_name, .department,
.documentation_url

Returns a url to view this cops documentation online.

.exclude_from_registry

Call for abstract Cop classes.

.inherited,
.joining_forces

Override and return the Force class(es) you need to join.

.match?

Returns true if the cop name or the cop namespace matches any of the given names.

.new,
.requires_gem

Register a version requirement for the given gem name.

.restrict_on_send

::RuboCop::ExcludeLimit - Extended

exclude_limit

Sets up a configuration option to have an exclude limit tracked.

transform

Instance Attribute Summary

Instance Method Summary

::RuboCop::Cop::Base - Inherited

#add_global_offense

Adds an offense that has no particular location.

#add_offense

Adds an offense on the specified range (or node with an expression) Unless that offense is disabled for this range, a corrector will be yielded to provide the cop the opportunity to autocorrect the offense.

#begin_investigation

Called before any investigation.

#callbacks_needed,
#cop_config

Configuration Helpers.

#cop_name, #excluded_file?,
#external_dependency_checksum

This method should be overridden when a cop’s behavior depends on state that lives outside of these locations:

#inspect,
#message

Gets called if no message is specified when calling add_offense or add_global_offense Cops are discouraged to override this; instead pass your message directly.

#name

Alias for Base#cop_name.

#offenses,
#on_investigation_end

Called after all on_…​

#on_new_investigation

Called before all on_…​

#on_other_file

Called instead of all on_…​

#parse

There should be very limited reasons for a Cop to do it’s own parsing.

#parser_engine,
#ready

Called between investigations.

#relevant_file?,
#target_gem_version

Returns a gems locked versions (i.e.

#target_rails_version, #target_ruby_version, #annotate, #apply_correction, #attempt_correction,
#callback_argument

Reserved for Cop::Cop.

#complete_investigation

Called to complete an investigation.

#correct, #current_corrector,
#current_offense_locations

Reserved for Commissioner:

#current_offenses, #currently_disabled_lines, #custom_severity, #default_severity, #disable_uncorrectable, #enabled_line?, #file_name_matches_any?, #find_message, #find_severity, #range_for_original, #range_from_node_or_range,
#reset_investigation

Actually private methods.

#use_corrector

::RuboCop::Cop::AutocorrectLogic - Included

::RuboCop::Cop::IgnoredNode - Included

Constructor Details

This class inherits a constructor from RuboCop::Cop::Base

Instance Method Details

#all_fields_literal?(string, arguments) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 125

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

#argument_value(argument) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 212

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

#argument_values(arguments) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 208

def argument_values(arguments)
  arguments.map { |argument| argument_value(argument) }
end

#complex_number?(node)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 66

def_node_matcher :complex_number?, <<~PATTERN
  {complex (send int :+ complex) (begin complex) (begin (send int :+ complex))}
PATTERN

#complex_value(complex_node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 244

def complex_value(complex_node)
  Complex(complex_node.source)
end

#detect_unnecessary_fields(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 102

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

#dsym_value(dsym_node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 230

def dsym_value(dsym_node)
  dsym_node.children.first.source
end

#find_argument(sequence, arguments, hash) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 143

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

#find_hash_value_node(node, name)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 71

def_node_search  :find_hash_value_node, <<~PATTERN
  (pair (sym %1) $_)
PATTERN

#float?(argument) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 184

def float?(argument)
  numeric?(argument) && Float(argument_value(argument), exception: false)
end

#format_without_additional_args?(node)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 56

def_node_matcher :format_without_additional_args?, <<~PATTERN
  (send {(const {nil? cbase} :Kernel) nil?} %RESTRICT_ON_SEND ${str dstr})
PATTERN

#hash_value(hash_node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 234

def hash_value(hash_node)
  hash_node.each_pair.with_object({}) do |pair, hash|
    hash[pair.key.value] = argument_value(pair.value)
  end
end

#integer?(argument) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 180

def integer?(argument)
  numeric?(argument) && Integer(argument_value(argument), exception: false)
end

#matching_argument?(sequence, argument) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 156

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

#message(node, prefer) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 98

def message(node, prefer)
  format(MSG, prefer: prefer, method_name: node.method_name)
end

#numeric?(argument) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 174

def numeric?(argument)
  argument.type?(:numeric, :str) ||
    rational_number?(argument) ||
    complex_number?(argument)
end

#on_send(node)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 83

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

#quote(string, node) (private)

Add correct quotes to the formatted string, preferring retaining the existing quotes if possible.

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 190

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

#rational_number?(node)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 61

def_node_matcher :rational_number?, <<~PATTERN
  {rational (send int :/ rational) (begin rational) (begin (send int :/ rational))}
PATTERN

#rational_value(rational_node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 240

def rational_value(rational_node)
  rational_node.source.to_r
end

#register_all_fields_literal(node, string, arguments) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 114

def register_all_fields_literal(node, string, arguments)
  return unless all_fields_literal?(string, arguments.dup)

  formatted_string = format(string, *argument_values(arguments))
  replacement = quote(formatted_string, node)

  add_offense(node, message: message(node, replacement)) do |corrector|
    corrector.replace(node, replacement)
  end
end

#splatted_arguments?(node)

[ GitHub ]

  
# File 'lib/rubocop/cop/style/redundant_format.rb', line 76

def_node_matcher :splatted_arguments?, <<~PATTERN
  (send _ %RESTRICT_ON_SEND <{
    splat
    (hash <kwsplat ...>)
  } ...>)
PATTERN