class RuboCop::Cop::Lint::FormatParameterMismatch
format(‘Numbered format: %1$s and numbered %2$s’, a_value, another)
# good
@example
format(‘Unnumbered format: %s and numbered: %2$s’, a_value, another)
# bad
@example
format(‘A value: %s and another: %i’, a_value, another)
# good
@example
format(‘A value: %s and another: %i’, a_value)
# bad
@example
the same format string.
format string. Do not mix numbered, unnumbered, and named formats in
In addition it checks whether different formats are used in the same
passed as arguments.
expected fields for format/sprintf/#% and what is actually
This lint sees if there is a mismatch between the number of
def count_format_matches(node)
def count_format_matches(node) [node.arguments.count - 1, expected_fields_count(node.first_argument)] end
def count_matches(node)
def count_matches(node) if countable_format?(node) count_format_matches(node) elsif countable_percent?(node) count_percent_matches(node) else [:unknown] * 2 end end
def count_percent_matches(node)
def count_percent_matches(node) [node.first_argument.child_nodes.count, expected_fields_count(node.receiver)] end
def countable_format?(node)
def countable_format?(node) (sprintf?(node) || format?(node)) && !heredoc?(node) end
def countable_percent?(node)
def countable_percent?(node) percent?(node) && node.first_argument.array_type? end
def expected_fields_count(node)
def expected_fields_count(node) return :unknown unless node.str_type? format_string = RuboCop::Cop::Utils::FormatString.new(node.source) return 1 if format_string.named_interpolation? max_digit_dollar_num = format_string.max_digit_dollar_num return max_digit_dollar_num if max_digit_dollar_num&.nonzero? format_string .format_sequences .reject(&:percent?) .reduce(0) { |acc, seq| acc + seq.arity } end
def format?(node)
def format?(node) format_method?(:format, node) end
def format_method?(name, node)
def format_method?(name, node) return false if node.const_receiver? && !node.receiver.loc.name.is?(KERNEL) return false unless node.method?(name) node.arguments.size > 1 && node.first_argument.str_type? end
def format_string?(node)
def format_string?(node) called_on_string?(node) && method_with_format_args?(node) end
def heredoc?(node)
def heredoc?(node) node.first_argument.source[0, 2] == SHOVEL end
def invalid_format_string?(node)
def invalid_format_string?(node) string = if sprintf?(node) || format?(node) node.first_argument.source else node.receiver.source end !RuboCop::Cop::Utils::FormatString.new(string).valid? end
def matched_arguments_count?(expected, passed)
def matched_arguments_count?(expected, passed) if passed.negative? expected < passed.abs else expected != passed end end
def message(node)
def message(node) num_args_for_format, num_expected_fields = count_matches(node) method_name = node.method?(:%) ? 'String#%' : node.method_name format(MSG, arg_num: num_args_for_format, method: method_name, field_num: num_expected_fields) end
def method_with_format_args?(node)
def method_with_format_args?(node) sprintf?(node) || format?(node) || percent?(node) end
def offending_node?(node)
def offending_node?(node) return false if splat_args?(node) num_of_format_args, num_of_expected_fields = count_matches(node) return false if num_of_format_args == :unknown matched_arguments_count?(num_of_expected_fields, num_of_format_args) end
def on_send(node)
def on_send(node) return unless format_string?(node) if invalid_format_string?(node) add_offense(node.loc.selector, message: MSG_INVALID) return end return unless offending_node?(node) add_offense(node.loc.selector, message: message(node)) end
def percent?(node)
def percent?(node) receiver = node.receiver percent = node.method?(:%) && (STRING_TYPES.include?(receiver.type) || node.first_argument.array_type?) return false if percent && STRING_TYPES.include?(receiver.type) && heredoc?(node) percent end
def splat_args?(node)
def splat_args?(node) return false if percent?(node) node.arguments.drop(1).any?(&:splat_type?) end
def sprintf?(node)
def sprintf?(node) format_method?(:sprintf, node) end