diff options
Diffstat (limited to 'rubocop/cop/static_translation_definition.rb')
-rw-r--r-- | rubocop/cop/static_translation_definition.rb | 121 |
1 files changed, 88 insertions, 33 deletions
diff --git a/rubocop/cop/static_translation_definition.rb b/rubocop/cop/static_translation_definition.rb index 3475a2b3dca..121b0c18770 100644 --- a/rubocop/cop/static_translation_definition.rb +++ b/rubocop/cop/static_translation_definition.rb @@ -2,63 +2,118 @@ module RuboCop module Cop + # This cop flags translation definitions in static scopes because changing + # locales has no effect and won't translate this text again. + # + # See https://docs.gitlab.com/ee/development/i18n/externalization.html#keep-translations-dynamic + # + # @example + # + # # bad + # class MyExample + # # Constant + # Translation = _('A translation.') + # + # # Class scope + # field :foo, title: _('A title') + # + # validates :title, :presence, message: _('is missing') + # + # # Memoized + # def self.translations + # @cached ||= { text: _('A translation.') } + # end + # + # included do # or prepended or class_methods + # self.error_message = _('Something went wrong.') + # end + # end + # + # # good + # class MyExample + # # Keep translations dynamic. + # Translation = -> { _('A translation.') } + # # OR + # def translation + # _('A translation.') + # end + # + # field :foo, title: -> { _('A title') } + # + # validates :title, :presence, message: -> { _('is missing') } + # + # def self.translations + # { text: _('A translation.') } + # end + # + # included do # or prepended or class_methods + # self.error_message = -> { _('Something went wrong.') } + # end + # end + # class StaticTranslationDefinition < RuboCop::Cop::Cop - MSG = "The text you're translating will be already in the translated form when it's assigned to the constant. When a users changes the locale, these texts won't be translated again. Consider moving the translation logic to a method." + MSG = <<~TEXT.tr("\n", ' ') + Translation is defined in static scope. + Keep translations dynamic. See https://docs.gitlab.com/ee/development/i18n/externalization.html#keep-translations-dynamic + TEXT - TRANSLATION_METHODS = %i[_ s_ n_].freeze + RESTRICT_ON_SEND = %i[_ s_ n_].freeze - def_node_matcher :translation_method?, <<~PATTERN - (send _ _ str*) - PATTERN + # List of method names which are not considered real method definitions. + # See https://api.rubyonrails.org/classes/ActiveSupport/Concern.html + NON_METHOD_DEFINITIONS = %i[class_methods included prepended].to_set.freeze - def_node_matcher :lambda_node?, <<~PATTERN - (send _ :lambda) - PATTERN - - def_node_matcher :struct_constant_assignment?, <<~PATTERN - (casgn _ _ `(const _ :Struct)) + def_node_matcher :translation_method?, <<~PATTERN + (send _ {#{RESTRICT_ON_SEND.map(&:inspect).join(' ')}} str*) PATTERN def on_send(node) return unless translation_method?(node) - method_name = node.children[1] - return unless TRANSLATION_METHODS.include?(method_name) - - translation_memoized = false + static = true + memoized = false node.each_ancestor do |ancestor| - receiver, _ = *ancestor - break if lambda_node?(receiver) # translations defined in lambda nodes should be allowed - - if constant_assignment?(ancestor) && !struct_constant_assignment?(ancestor) - add_offense(node, location: :expression) + memoized = true if memoized?(ancestor) + if dynamic?(ancestor, memoized) + static = false break end + end + + add_offense(node) if static + end - translation_memoized = true if memoization?(ancestor) + private - if translation_memoized && class_method_definition?(ancestor) - add_offense(node, location: :expression) + def memoized?(node) + node.type == :or_asgn + end - break - end - end + def dynamic?(node, memoized) + lambda_or_proc?(node) || + named_block?(node) || + instance_method_definition?(node) || + unmemoized_class_method_definition?(node, memoized) end - private + def lambda_or_proc?(node) + node.lambda_or_proc? + end - def constant_assignment?(node) - node.type == :casgn + def named_block?(node) + return unless node.block_type? + + !NON_METHOD_DEFINITIONS.include?(node.method_name) # rubocop:disable Rails/NegateInclude end - def memoization?(node) - node.type == :or_asgn + def instance_method_definition?(node) + node.type == :def end - def class_method_definition?(node) - node.type == :defs + def unmemoized_class_method_definition?(node, memoized) + node.type == :defs && !memoized end end end |