# frozen_string_literal: truerequire"rubocop"moduleRuboCopmoduleCopmoduleSorbet# Checks that every Ruby file contains a valid Sorbet sigil.# Adapted from: https://gist.github.com/clarkdave/85aca4e16f33fd52aceb6a0a29936e52## Options:## * `RequireSigilOnAllFiles`: make offense if the Sorbet typed is not found in the file (default: false)# * `SuggestedStrictness`: Sorbet strictness level suggested in offense messages (default: 'false')# * `MinimumStrictness`: If set, make offense if the strictness level in the file is below this one# * `ExactStrictness`: If set, make offense if the strictness level in the file is different than this one## If an `ExactStrictness` level is specified, it will be used in offense messages and autocorrect.# If a `SuggestedStrictness` level is specified, it will be used in autocorrect.# Otherwise, if a `MinimumStrictness` level is specified, it will be used in offense messages and autocorrect.classValidSigil<RuboCop::Cop::BaseextendAutoCorrectordefon_new_investigationreturnifprocessed_source.tokens.empty?sigil=extract_sigil(processed_source)returnunlesscheck_sigil_present(sigil)strictness=extract_strictness(sigil)check_double_commented_sigil(sigil,strictness)returnunlesscheck_strictness_not_empty(sigil,strictness)returnunlesscheck_strictness_valid(sigil,strictness)nilunlesscheck_strictness_level(sigil,strictness)endprotectedSTRICTNESS_LEVELS=["ignore","false","true","strict","strong"]SIGIL_REGEX=/^[[:blank:]]*(?:#[[:blank:]]*)?#[[:blank:]]+typed:(?:[[:blank:]]+([\S]+))?/INVALID_SIGIL_MSG="Invalid Sorbet sigil `%<sigil>s`."# extractiondefextract_sigil(processed_source)processed_source.tokens.take_while{|token|token.type==:tCOMMENT}.find{|token|SIGIL_REGEX.match?(token.text)}enddefextract_strictness(sigil)sigil.text.match(SIGIL_REGEX)&.captures&.firstend# checksdefcheck_sigil_present(sigil)returntrueunlesssigil.nil?token=processed_source.tokens.firstifrequire_sigil_on_all_files?strictness=suggested_strictness_leveladd_offense(token.pos,message: "No Sorbet sigil found in file. "\"Try a `typed: #{strictness}` to start (you can also use `rubocop -a` to automatically add this).",)do|corrector|autocorrect(corrector)endendfalseenddefsuggested_strictness_levelreturnexact_strictnessifexact_strictness# if no minimum strictness is set (eg. using Sorbet/HasSigil without config) then# we always use the suggested strictness which defaults to `false`returnsuggested_strictnessunlessminimum_strictness# special case: if you're using Sorbet/IgnoreSigil without config, we should recommend `ignore`return"ignore"ifminimum_strictness=="ignore"&&cop_config["SuggestedStrictness"].nil?# if a minimum strictness is set (eg. you're using Sorbet/FalseSigil)# we want to compare the minimum strictness and suggested strictness. this is because# the suggested strictness might be higher than the minimum (eg. if you want all new files# at a higher strictness level, without having to migrate existing files at lower levels).levels=[STRICTNESS_LEVELS.index(suggested_strictness),STRICTNESS_LEVELS.index(minimum_strictness),]STRICTNESS_LEVELS[levels.compact.max]enddefcheck_strictness_not_empty(sigil,strictness)returntrueifstrictnessadd_offense(sigil.pos,message: "Sorbet sigil should not be empty.",)do|corrector|autocorrect(corrector)endfalseenddefcheck_double_commented_sigil(sigil,strictness)returnunlesssigil.text.start_with?(/[[:blank:]]*#[[:blank:]]*#/)add_offense(sigil.pos,message: format(INVALID_SIGIL_MSG,sigil: sigil.text),)do|corrector|# Remove the extra comment and normalize spacescorrector.replace(sigil.pos,"# typed: #{strictness}",)endenddefcheck_strictness_valid(sigil,strictness)returntrueifSTRICTNESS_LEVELS.include?(strictness)add_offense(sigil.pos,message: format(INVALID_SIGIL_MSG,sigil: strictness),)do|corrector|autocorrect(corrector)endfalseenddefcheck_strictness_level(sigil,strictness)returntrueif!minimum_strictness&&!exact_strictnesscurrent_level=STRICTNESS_LEVELS.index(strictness)ifexact_strictnessexact_level=STRICTNESS_LEVELS.index(exact_strictness)ifcurrent_level!=exact_leveladd_offense(sigil.pos,message: "Sorbet sigil should be `#{exact_strictness}` got `#{strictness}`.",)do|corrector|autocorrect(corrector)endreturnfalseendelseminimum_level=STRICTNESS_LEVELS.index(minimum_strictness)ifcurrent_level<minimum_leveladd_offense(sigil.pos,message: "Sorbet sigil should be at least `#{minimum_strictness}` got `#{strictness}`.",)do|corrector|autocorrect(corrector)endreturnfalseendendtrueenddefautocorrect(corrector)returnunlessrequire_sigil_on_all_files?returnunlessextract_sigil(processed_source).nil?token=processed_source.tokens.firstreplace_with=suggested_strictness_levelsigil="# typed: #{replace_with}"iftoken.text.start_with?("#!")# shebang linecorrector.insert_after(token.pos,"\n#{sigil}")elsecorrector.insert_before(token.pos,"#{sigil}\n")endend# options# Default is `false`defrequire_sigil_on_all_files?!!cop_config["RequireSigilOnAllFiles"]end# Default is `'false'`defsuggested_strictnessconfig=cop_config["SuggestedStrictness"].to_sSTRICTNESS_LEVELS.include?(config)?config:"false"end# Default is `nil`defminimum_strictnessconfig=cop_config["MinimumStrictness"].to_sconfigifSTRICTNESS_LEVELS.include?(config)end# Default is `nil`defexact_strictnessconfig=cop_config["ExactStrictness"].to_sconfigifSTRICTNESS_LEVELS.include?(config)endendendendend