1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
|
# 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(' ')}} {dstr 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
|