# frozen_string_literal: true module RuboCop module Cop module Utils # Parses {Kernel#sprintf} format strings. class FormatString DIGIT_DOLLAR = /(\d+)\$/.freeze FLAG = /[ #0+-]|#{DIGIT_DOLLAR}/.freeze NUMBER_ARG = /\*#{DIGIT_DOLLAR}?/.freeze NUMBER = /\d+|#{NUMBER_ARG}/.freeze WIDTH = /(?#{NUMBER})/.freeze PRECISION = /\.(?#{NUMBER})/.freeze TYPE = /(?[bBdiouxXeEfgGaAcps])/.freeze NAME = /<(?\w+)>/.freeze TEMPLATE_NAME = /\{(?\w+)\}/.freeze SEQUENCE = / % (?%) | % (?#{FLAG}*) (?: (?: #{WIDTH}? #{PRECISION}? #{NAME}? | #{WIDTH}? #{NAME} #{PRECISION}? | #{NAME} (?#{FLAG}*) #{WIDTH}? #{PRECISION}? ) #{TYPE} | #{WIDTH}? #{PRECISION}? #{TEMPLATE_NAME} ) /x.freeze # The syntax of a format sequence is as follows. # # ``` # %[flags][width][.precision]type # ``` # # A format sequence consists of a percent sign, followed by optional # flags, width, and precision indicators, then terminated with a field # type character. # # For more complex formatting, Ruby supports a reference by name. # # @see https://ruby-doc.org/core-2.6.3/Kernel.html#method-i-format class FormatSequence attr_reader :begin_pos, :end_pos attr_reader :flags, :width, :precision, :name, :type def initialize(string, **opts) @source = string @begin_pos = opts[:begin_pos] @end_pos = opts[:end_pos] @flags = opts[:flags] @width = opts[:width] @precision = opts[:precision] @name = opts[:name] @type = opts[:type] end def percent? type == '%' end def annotated? name && @source.include?('<') end def template? name && @source.include?('{') end # Number of arguments required for the format sequence def arity @source.scan('*').count + 1 end def max_digit_dollar_num @source.scan(DIGIT_DOLLAR).map do |(digit_dollar_num)| digit_dollar_num.to_i end.max end def style if annotated? :annotated elsif template? :template else :unannotated end end end def initialize(string) @source = string end def format_sequences @format_sequences ||= parse end def named_interpolation? format_sequences.any?(&:name) end def max_digit_dollar_num format_sequences.map(&:max_digit_dollar_num).max end private def parse @source.to_enum(:scan, SEQUENCE).map do match = Regexp.last_match FormatSequence.new( match[0], begin_pos: match.begin(0), end_pos: match.end(0), flags: match[:flags].to_s + match[:more_flags].to_s, width: match[:width], precision: match[:precision], name: match[:name], type: match[:type] ) end end end end end end