# frozen_string_literal: truemoduleRuboCopmoduleCopmoduleStyle# Use a consistent style for named format string tokens.## **Note:**# `unannotated` style cop only works for strings# which are passed as arguments to those methods:# `sprintf`, `format`, `%`.# The reason is that *unannotated* format is very similar# to encoded URLs or Date/Time formatting strings.## @example EnforcedStyle: annotated (default)## # bad# format('%{greeting}', greeting: 'Hello')# format('%s', 'Hello')## # good# format('%<greeting>s', greeting: 'Hello')## @example EnforcedStyle: template## # bad# format('%<greeting>s', greeting: 'Hello')# format('%s', 'Hello')## # good# format('%{greeting}', greeting: 'Hello')## @example EnforcedStyle: unannotated## # bad# format('%<greeting>s', greeting: 'Hello')# format('%{greeting}', 'Hello')## # good# format('%s', 'Hello')classFormatStringToken<CopincludeConfigurableEnforcedStyleFIELD_CHARACTERS=Regexp.union(%w[A B E G X a b c d e f g i o p s u x])FORMAT_STRING_METHODS=%i[sprintf format %].freezeSTYLE_PATTERNS={annotated: /(?<token>%<[^>]+>#{FIELD_CHARACTERS})/,template: /(?<token>%\{[^\}]+\})/,unannotated: /(?<token>%#{FIELD_CHARACTERS})/}.freezedefon_str(node)returnifplaceholder_argument?(node)returnifnode.each_ancestor(:xstr,:regexp).any?tokens(node)do|detected_style,token_range|ifdetected_style==style||unannotated_format?(node,detected_style)correct_style_detectedelsestyle_detected(detected_style)add_offense(node,location: token_range,message: message(detected_style))endendendprivatedefincludes_format_methods?(node)node.each_ancestor(:send).any?do|ancestor|FORMAT_STRING_METHODS.include?(ancestor.method_name)endenddefunannotated_format?(node,detected_style)detected_style==:unannotated&&!includes_format_methods?(node)enddefmessage(detected_style)"Prefer #{message_text(style)} over #{message_text(detected_style)}."end# rubocop:disable Style/FormatStringTokendefmessage_text(style)casestylewhen:annotatedthen'annotated tokens (like `%<foo>s`)'when:templatethen'template tokens (like `%{foo}`)'when:unannotatedthen'unannotated tokens (like `%s`)'endend# rubocop:enable Style/FormatStringTokendeftokens(str_node,&block)returnifstr_node.source=='__FILE__'token_ranges(str_contents(str_node.loc),&block)enddefstr_contents(source_map)ifsource_map.is_a?(Parser::Source::Map::Heredoc)source_map.heredoc_bodyelsifsource_map.beginslice_source(source_map.expression,source_map.expression.begin_pos+1,source_map.expression.end_pos-1)elsesource_map.expressionendenddeftoken_ranges(contents)while(offending_match=match_token(contents))detected_style,*range=*offending_matchtoken,contents=split_token(contents,*range)yield(detected_style,token)endenddefmatch_token(source_range)supported_styles.eachdo|style_name|pattern=STYLE_PATTERNS.fetch(style_name)match=source_range.source.match(pattern)nextunlessmatchreturn[style_name,match.begin(:token),match.end(:token)]endnilenddefsplit_token(source_range,match_begin,match_end)token=slice_source(source_range,source_range.begin_pos+match_begin,source_range.begin_pos+match_end)remainder=slice_source(source_range,source_range.begin_pos+match_end,source_range.end_pos)[token,remainder]enddefslice_source(source_range,new_begin,new_end)Parser::Source::Range.new(source_range.source_buffer,new_begin,new_end)enddefplaceholder_argument?(node)returnfalseunlessnode.parentreturntrueifnode.parent.pair_type?placeholder_argument?(node.parent)endendendendend