# frozen_string_literal: truemoduleRuboCopmoduleCopmoduleNaming# Recommends the use of inclusive language instead of problematic terms.# The cop can check the following locations for offenses:## - identifiers# - constants# - variables# - strings# - symbols# - comments# - file paths## Each of these locations can be individually enabled/disabled via configuration,# for example CheckIdentifiers = true/false.## Flagged terms are configurable for the cop. For each flagged term an optional# Regex can be specified to identify offenses. Suggestions for replacing a flagged term can# be configured and will be displayed as part of the offense message.# An AllowedRegex can be specified for a flagged term to exempt allowed uses of the term.# `WholeWord: true` can be set on a flagged term to indicate the cop should only match when# a term matches the whole word (partial matches will not be offenses).## The cop supports autocorrection when there is only one suggestion. When there are multiple# suggestions, the best suggestion cannot be identified and will not be autocorrected.## @example FlaggedTerms: { whitelist: { Suggestions: ['allowlist'] } }# # Suggest replacing identifier whitelist with allowlist## # bad# whitelist_users = %w(user1 user1)## # good# allowlist_users = %w(user1 user2)## @example FlaggedTerms: { master: { Suggestions: ['main', 'primary', 'leader'] } }# # Suggest replacing master in an instance variable name with main, primary, or leader## # bad# @master_node = 'node1.example.com'## # good# @primary_node = 'node1.example.com'## @example FlaggedTerms: { whitelist: { Regex: !ruby/regexp '/white[-_\s]?list' } }# # Identify problematic terms using a Regexp## # bad# white_list = %w(user1 user2)## # good# allow_list = %w(user1 user2)## @example FlaggedTerms: { master: { AllowedRegex: 'master\'?s degree' } }# # Specify allowed uses of the flagged term as a string or regexp.## # bad# # They had a masters## # good# # They had a master's degree## @example FlaggedTerms: { slave: { WholeWord: true } }# # Specify that only terms that are full matches will be flagged.## # bad# Slave## # good (won't be flagged despite containing `slave`)# TeslaVehicleclassInclusiveLanguage<BaseincludeRangeHelpextendAutoCorrectorEMPTY_ARRAY=[].freezeMSG="Consider replacing '%<term>s'%<suffix>s."MSG_FOR_FILE_PATH="Consider replacing '%<term>s' in file path%<suffix>s."WordLocation=Struct.new(:word,:position)definitialize(config=nil,options=nil)super@flagged_term_hash={}@flagged_terms_regex=nil@allowed_regex=nil@check_token=preprocess_check_configpreprocess_flagged_termsenddefon_new_investigationinvestigate_filepathifcop_config['CheckFilepaths']investigate_tokensendprivatedefinvestigate_tokensprocessed_source.tokens.eachdo|token|nextunlesscheck_token?(token.type)word_locations=scan_for_words(token.text)nextifword_locations.empty?add_offenses_for_token(token,word_locations)endenddefadd_offenses_for_token(token,word_locations)word_locations.eachdo|word_location|word=word_location.wordrange=offense_range(token,word)add_offense(range,message: create_message(word))do|corrector|suggestions=find_flagged_term(word)['Suggestions']nextunlesssuggestions.is_a?(String)corrector.replace(range,suggestions)endendenddefcheck_token?(type)!!@check_token[type]enddefpreprocess_check_config# rubocop:disable Metrics/AbcSize{tIDENTIFIER: cop_config['CheckIdentifiers'],tCONSTANT: cop_config['CheckConstants'],tIVAR: cop_config['CheckVariables'],tCVAR: cop_config['CheckVariables'],tGVAR: cop_config['CheckVariables'],tSYMBOL: cop_config['CheckSymbols'],tSTRING: cop_config['CheckStrings'],tSTRING_CONTENT: cop_config['CheckStrings'],tCOMMENT: cop_config['CheckComments']}.freezeenddefpreprocess_flagged_termsallowed_strings=[]flagged_term_strings=[]cop_config['FlaggedTerms'].eachdo|term,term_definition|nextifterm_definition.nil?allowed_strings.concat(process_allowed_regex(term_definition['AllowedRegex']))regex_string=ensure_regex_string(extract_regexp(term,term_definition))flagged_term_strings<<regex_stringadd_to_flagged_term_hash(regex_string,term,term_definition)endset_regexes(flagged_term_strings,allowed_strings)enddefextract_regexp(term,term_definition)returnterm_definition['Regex']ifterm_definition['Regex']return/(?:\b|(?<=[\W_]))#{term}(?:\b|(?=[\W_]))/ifterm_definition['WholeWord']termenddefadd_to_flagged_term_hash(regex_string,term,term_definition)@flagged_term_hash[Regexp.new(regex_string,Regexp::IGNORECASE)]=term_definition.merge('Term'=>term,'SuggestionString'=>preprocess_suggestions(term_definition['Suggestions']))enddefset_regexes(flagged_term_strings,allowed_strings)@flagged_terms_regex=array_to_ignorecase_regex(flagged_term_strings)@allowed_regex=array_to_ignorecase_regex(allowed_strings)unlessallowed_strings.empty?enddefprocess_allowed_regex(allowed)returnEMPTY_ARRAYifallowed.nil?Array(allowed).mapdo|allowed_term|nextifallowed_term.is_a?(String)&&allowed_term.strip.empty?ensure_regex_string(allowed_term)endenddefensure_regex_string(regex)regex.is_a?(Regexp)?regex.source:regexenddefarray_to_ignorecase_regex(strings)Regexp.new(strings.join('|'),Regexp::IGNORECASE)enddefinvestigate_filepathword_locations=scan_for_words(processed_source.file_path)caseword_locations.lengthwhen0returnwhen1message=create_single_word_message_for_file(word_locations.first.word)elsewords=word_locations.map(&:word)message=create_multiple_word_message_for_file(words)endrange=source_range(processed_source.buffer,1,0)add_offense(range,message: message)enddefcreate_single_word_message_for_file(word)create_message(word,MSG_FOR_FILE_PATH)enddefcreate_multiple_word_message_for_file(words)format(MSG_FOR_FILE_PATH,term: words.join("', '"),suffix: ' with other terms')enddefscan_for_words(input)masked_input=mask_input(input)returnEMPTY_ARRAYunlessmasked_input.match?(@flagged_terms_regex)masked_input.enum_for(:scan,@flagged_terms_regex).mapdomatch=Regexp.last_matchWordLocation.new(match.to_s,match.offset(0).first)endenddefmask_input(str)safe_str=ifstr.valid_encoding?strelsestr.encode('UTF-8',invalid: :replace,undef: :replace)endreturnsafe_strif@allowed_regex.nil?safe_str.gsub(@allowed_regex){|match|'*'*match.size}enddefcreate_message(word,message=MSG)flagged_term=find_flagged_term(word)suggestions=flagged_term['SuggestionString']suggestions=' with another term'ifsuggestions.blank?format(message,term: word,suffix: suggestions)enddeffind_flagged_term(word)_regexp,flagged_term=@flagged_term_hash.finddo|key,_term|key.match?(word)endflagged_termenddefpreprocess_suggestions(suggestions)return''ifsuggestions.nil?||(suggestions.is_a?(String)&&suggestions.strip.empty?)||suggestions.empty?format_suggestions(suggestions)enddefformat_suggestions(suggestions)quoted_suggestions=Array(suggestions).map{|word|"'#{word}'"}suggestion_str=casequoted_suggestions.sizewhen1quoted_suggestions.firstwhen2quoted_suggestions.join(' or ')elselast_quoted=quoted_suggestions.popquoted_suggestions<<"or #{last_quoted}"quoted_suggestions.join(', ')end" with #{suggestion_str}"enddefoffense_range(token,word)start_position=token.pos.begin_pos+token.pos.source.index(word)range_between(start_position,start_position+word.length)endendendendend