class RuboCop::Cop::Style::FormatStringToken
“%{greeting}” % { greeting: ‘Hello’ }
sprintf(“%{greeting}”, greeting: ‘Hello’)
printf(“%{greeting}”, greeting: ‘Hello’)
format(“%{greeting}”, greeting: ‘Hello’)
# bad
foo(“%{greeting}”)
“%{greeting}”
# good
# given to a known formatting method.
# In ‘conservative` mode, offenses are only registered for strings
@example Mode: conservative, EnforcedStyle: annotated
redirect(’foo/%{bar_id}‘)
# good
@example AllowedPatterns: [’redirect’]
redirect(‘foo/%{bar_id}’)
# bad
@example AllowedPatterns: [] (default)
redirect(‘foo/%{bar_id}’)
# good
@example AllowedMethods: [redirect]
redirect(‘foo/%{bar_id}’)
# bad
@example AllowedMethods: [] (default)
format(‘%06d’, 10)
# good
format(‘%s %s.’, ‘Hello’, ‘world’)
# bad
@example MaxUnannotatedPlaceholdersAllowed: 1 (default)
format(‘%<number>06d’, number: 10)
# good
format(‘%s %s.’, ‘Hello’, ‘world’)
format(‘%06d’, 10)
# bad
@example MaxUnannotatedPlaceholdersAllowed: 0
`MaxUnannotatedPlaceholdersAllowed`.
if the number of them is less than or equals to
It is allowed to contain unannotated token
format(‘%s’, ‘Hello’)
# good
format(‘%{greeting}’, greeting: ‘Hello’)
format(‘%<greeting>s’, greeting: ‘Hello’)
# bad
@example EnforcedStyle: unannotated
format(‘%{greeting}’, greeting: ‘Hello’)
# good
format(‘%s’, ‘Hello’)
format(‘%<greeting>s’, greeting: ‘Hello’)
# bad
@example EnforcedStyle: template
format(‘%<greeting>s’, greeting: ‘Hello’)
# good
format(‘%s’, ‘Hello’)
format(‘%{greeting}’, greeting: ‘Hello’)
# bad
@example EnforcedStyle: annotated (default)
because this format is very similar to encoded URLs or Date/Time formatting strings.
configured with ‘Conservative: true`. This is done in order to prevent false positives,
NOTE: Tokens in the `unannotated` style (eg. `%s`) are always treated as if
methods `printf`, `sprintf`, `format` and `%`.
of `EnforcedStyle`) are only considered if used in the format string argument to the
`Mode: conservative` (default `aggressive`). In this mode, tokens (regardless
Additionally, the cop can be made conservative by configuring it with
are no allowed methods.
methods as always allowed, thereby avoiding an offense from the cop. By default, there
`AllowedMethods` or `AllowedPatterns` can be configured with in order to mark specific
them to be tokens, but rather other identifiers or just part of the string.
as they could be used as arguments to a method that does not consider
By default, all strings are evaluated. In some cases, this may be undesirable,
Use a consistent style for tokens within a format string.
def allowed_string?(node, detected_style)
def allowed_string?(node, detected_style) (detected_style == :unannotated || conservative?) && !format_string_in_typical_context?(node) end
def allowed_unannotated?(detections)
def allowed_unannotated?(detections) return false unless detections.all? do |detected_sequence,| detected_sequence.style == :unannotated end return true if detections.size <= max_unannotated_placeholders_allowed detections.any? { |detected_sequence,| !correctable_sequence?(detected_sequence.type) } end
def autocorrect_sequence(corrector, detected_sequence, token_range)
def autocorrect_sequence(corrector, detected_sequence, token_range) return if style == :unannotated name = detected_sequence.name return if name.nil? flags = detected_sequence.flags width = detected_sequence.width precision = detected_sequence.precision type = detected_sequence.style == :template ? 's' : detected_sequence.type correction = case style when :annotated then "%<#{name}>#{flags}#{width}#{precision}#{type}" when :template then "%#{flags}#{width}#{precision}{#{name}}" end corrector.replace(token_range, correction) end
def check_sequence(detected_sequence, token_range)
def check_sequence(detected_sequence, token_range) if detected_sequence.style == style correct_style_detected elsif correctable_sequence?(detected_sequence.type) style_detected(detected_sequence.style) add_offense(token_range, message: message(detected_sequence.style)) do |corrector| autocorrect_sequence(corrector, detected_sequence, token_range) end end end
def collect_detections(node)
def collect_detections(node) detections = [] tokens(node) do |detected_sequence, token_range| unless allowed_string?(node, detected_sequence.style) detections << [detected_sequence, token_range] end end detections end
def conservative?
def conservative? cop_config.fetch('Mode', :aggressive).to_sym == :conservative end
def correctable_sequence?(detected_type)
def correctable_sequence?(detected_type) detected_type == 's' || style == :annotated || style == :unannotated end
def format_string_token?(node)
def format_string_token?(node) !node.value.include?('%') || node.each_ancestor(:xstr, :regexp).any? end
def max_unannotated_placeholders_allowed
def max_unannotated_placeholders_allowed cop_config['MaxUnannotatedPlaceholdersAllowed'] end
def message(detected_style)
def message(detected_style) "Prefer #{message_text(style)} over #{message_text(detected_style)}." end
def message_text(style)
def message_text(style) { annotated: 'annotated tokens (like `%<foo>s`)', template: 'template tokens (like `%{foo}`)', unannotated: 'unannotated tokens (like `%s`)' }[style] end
def on_str(node)
def on_str(node) return if format_string_token?(node) || use_allowed_method?(node) detections = collect_detections(node) return if detections.empty? return if allowed_unannotated?(detections) detections.each do |detected_sequence, token_range| check_sequence(detected_sequence, token_range) end end
def str_contents(source_map)
def str_contents(source_map) if source_map.is_a?(Parser::Source::Map::Heredoc) source_map.heredoc_body elsif source_map.begin source_map.expression.adjust(begin_pos: +1, end_pos: -1) else source_map.expression end end
def token_ranges(contents)
def token_ranges(contents) format_string = RuboCop::Cop::Utils::FormatString.new(contents.source) format_string.format_sequences.each do |detected_sequence| next if detected_sequence.percent? token = contents.begin.adjust(begin_pos: detected_sequence.begin_pos, end_pos: detected_sequence.end_pos) yield(detected_sequence, token) end end
def tokens(str_node, &block)
def tokens(str_node, &block) return if str_node.source == '__FILE__' token_ranges(str_contents(str_node.loc), &block) end
def use_allowed_method?(node)
def use_allowed_method?(node) send_parent = node.each_ancestor(:send).first send_parent && (allowed_method?(send_parent.method_name) || matches_allowed_pattern?(send_parent.method_name)) end