120 lines
3.2 KiB
Ruby
120 lines
3.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
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::Base
|
|
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
|
|
|
|
RESTRICT_ON_SEND = %i[_ s_ n_].freeze
|
|
|
|
# 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 :translation_method?, <<~PATTERN
|
|
(send _ {#{RESTRICT_ON_SEND.map(&:inspect).join(' ')}} str*)
|
|
PATTERN
|
|
|
|
def on_send(node)
|
|
return unless translation_method?(node)
|
|
|
|
static = true
|
|
memoized = false
|
|
|
|
node.each_ancestor do |ancestor|
|
|
memoized = true if memoized?(ancestor)
|
|
|
|
if dynamic?(ancestor, memoized)
|
|
static = false
|
|
break
|
|
end
|
|
end
|
|
|
|
add_offense(node) if static
|
|
end
|
|
|
|
private
|
|
|
|
def memoized?(node)
|
|
node.type == :or_asgn
|
|
end
|
|
|
|
def dynamic?(node, memoized)
|
|
lambda_or_proc?(node) ||
|
|
named_block?(node) ||
|
|
instance_method_definition?(node) ||
|
|
unmemoized_class_method_definition?(node, memoized)
|
|
end
|
|
|
|
def lambda_or_proc?(node)
|
|
node.lambda_or_proc?
|
|
end
|
|
|
|
def named_block?(node)
|
|
return unless node.block_type?
|
|
|
|
!NON_METHOD_DEFINITIONS.include?(node.method_name) # rubocop:disable Rails/NegateInclude
|
|
end
|
|
|
|
def instance_method_definition?(node)
|
|
node.type == :def
|
|
end
|
|
|
|
def unmemoized_class_method_definition?(node, memoized)
|
|
node.type == :defs && !memoized
|
|
end
|
|
end
|
|
end
|
|
end
|