rubocop-neeto 0.1.20 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a80cf5441b3f2e8a557f1ab80f1c33f234fd0964051e0c4ba3082ed6ad4ec038
4
- data.tar.gz: ed16bab4d596c2741d0205e1a3904c9207574564df4481b7aae7fac5541427f1
3
+ metadata.gz: 8f79480fcc020ba036f101838a8970627d0d6cee99fdf7db819c0eaf55430e8f
4
+ data.tar.gz: 510ea55903e4e085be783f0bfc618e5e28f725ac399c83d992ea03473b030dff
5
5
  SHA512:
6
- metadata.gz: f34434c7395250edfac2c36e73f719bbcf80f37443945e9a6e1fc31958dc1736aba5c3705eeea4f5fd455e72022e2f1e7465ce9810fc72e32a757840aaed0840
7
- data.tar.gz: 4c4e0f22d50814ceb614509f44218e0733a3466eccb7674d29cc200fed15e9bed465e99c1e8f0d91d2f0a548cccb5c060b3ab65cbf98011e0d7f6aed01f73869
6
+ metadata.gz: 8e93ef6aac8079e2a95f8994e63d34f826a3f5f56041d7285695323bcb3a1910c16027c6a1278c2a81c15d52454366f7a65e89619a14ef7c2592afcdbb499344
7
+ data.tar.gz: 266c32b005d5d7bfb3933f936dc27895e0d388db5e74dfbc4bcf52cfe3683ef58158c8a1f6240d5a7dbf680bd2631eea6fdac5ea949690de839c68a342295ef0
data/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
  8. [Neeto/DirectLiquidRender](https://rubocop-neeto.neetodeployapp.com/docs/RuboCop/Cop/Neeto/DirectLiquidRender)
13
13
  9. [Neeto/HtmlTranslationInterpolation](https://rubocop-neeto.neetodeployapp.com/docs/RuboCop/Cop/Neeto/HtmlTranslationInterpolation)
14
14
 
15
- The HTML-safety cops (6 to 9) report at `Severity: info` while products migrate, so they show in editors and CI output without failing the build. They move to `Severity: error` once every product is migrated.
15
+ The HTML-safety cops (6 to 9) report at `Severity: refactor`, like the other Neeto cops, so their offenses fail RuboCop and erb_lint at the default fail level.
16
16
 
17
17
  ## Installation
18
18
 
data/config/default.yml CHANGED
@@ -74,7 +74,7 @@ Neeto/HtmlSafeOnDynamicString:
74
74
  or `safe_join` instead, or annotate a reviewed call with
75
75
  `# neeto:html-safe-reviewed <reason>`.
76
76
  Enabled: true
77
- Severity: info
77
+ Severity: refactor
78
78
  VersionAdded: '0.1'
79
79
  Include:
80
80
  - app/**/*.rb
@@ -88,7 +88,7 @@ Neeto/HtmlInStringLiteral:
88
88
  markup outside views with `tag`, `content_tag`, `link_to`, `mail_to`,
89
89
  `safe_join` or a partial instead of string literals and heredocs.
90
90
  Enabled: true
91
- Severity: info
91
+ Severity: refactor
92
92
  VersionAdded: '0.1'
93
93
  Include:
94
94
  - app/**/*.rb
@@ -104,11 +104,12 @@ Neeto/DirectLiquidRender:
104
104
  escapes every output and requires an explicit format (`:html` or `:text`).
105
105
  Check a template's syntax with `NeetoCommonsBackend::LiquidRenderer.validate!`.
106
106
  Enabled: true
107
- Severity: info
107
+ Severity: refactor
108
108
  VersionAdded: '0.1'
109
109
  Include:
110
110
  - app/**/*.rb
111
111
  - lib/**/*.rb
112
+ - app/**/*.jbuilder
112
113
 
113
114
  Neeto/HtmlTranslationInterpolation:
114
115
  Description: >-
@@ -117,13 +118,15 @@ Neeto/HtmlTranslationInterpolation:
117
118
  Escape each value with `ERB::Util.html_escape`, or keep the markup out of
118
119
  the translation and build it with `tag` or `content_tag`.
119
120
  Enabled: true
120
- Severity: info
121
+ Severity: refactor
121
122
  VersionAdded: '0.1'
122
123
  LocaleFiles:
123
- - config/locales/en.yml
124
+ - config/locales/**/en.yml
125
+ - config/locales/**/*.en.yml
124
126
  Include:
125
127
  - app/**/*.rb
126
128
  - lib/**/*.rb
129
+ - app/views/**/*.erb
130
+ - app/views/**/*.inky
127
131
  Exclude:
128
- - app/views/**/*
129
132
  - lib/tasks/**/*
@@ -49,7 +49,7 @@ module RuboCop
49
49
  def on_dstr(node)
50
50
  return if part_of_another_literal?(node)
51
51
 
52
- check(node, node.each_child_node(:str).map(&:value).join)
52
+ check(node, literal_text(node))
53
53
  end
54
54
 
55
55
  private
@@ -61,6 +61,10 @@ module RuboCop
61
61
  add_offense(node)
62
62
  end
63
63
 
64
+ def literal_text(node)
65
+ node.each_child_node(:str, :dstr).map { |child| child.str_type? ? child.value : literal_text(child) }.join
66
+ end
67
+
64
68
  def part_of_another_literal?(node)
65
69
  ENCLOSING_LITERAL_TYPES.include?(node.parent&.type)
66
70
  end
@@ -37,6 +37,10 @@ module RuboCop
37
37
 
38
38
  RESTRICT_ON_SEND = %i[html_safe raw safe_concat].freeze
39
39
 
40
+ def_node_matcher :view_helpers?, <<~PATTERN
41
+ (send _ {:helpers :view_context})
42
+ PATTERN
43
+
40
44
  def on_send(node)
41
45
  value = trusted_value(node)
42
46
  return if value.nil? || static_string?(value) || reviewed?(node)
@@ -52,7 +56,7 @@ module RuboCop
52
56
  when :html_safe
53
57
  node.receiver if node.arguments.none?
54
58
  when :raw
55
- node.first_argument if node.receiver.nil? && node.arguments.one?
59
+ node.first_argument if (node.receiver.nil? || view_helpers?(node.receiver)) && node.arguments.one?
56
60
  when :safe_concat
57
61
  node.first_argument if node.arguments.one?
58
62
  end
@@ -7,8 +7,9 @@ module RuboCop
7
7
  module Neeto
8
8
  # `I18n.t` does not escape interpolated values, even for `_html` keys
9
9
  # (only the view helper `t` does). A translation that contains HTML and
10
- # interpolates an unescaped value outside a view lets that value inject
11
- # markup. The cop reads the translations from `LocaleFiles`.
10
+ # interpolates an unescaped value lets that value inject markup. In views,
11
+ # the `t` helper escapes the values of `_html` keys, so only those are
12
+ # allowed there. The cop reads the translations from `LocaleFiles`.
12
13
  #
13
14
  # @example HtmlTranslationInterpolation: true (default)
14
15
  # # en.yml
@@ -24,6 +25,12 @@ module RuboCop
24
25
  # # good
25
26
  # tag.p(I18n.t("seed.description_text", name: user.name))
26
27
  #
28
+ # # bad (in a view)
29
+ # <%= sanitize t("seed.description", name: @name) %>
30
+ #
31
+ # # good (in a view)
32
+ # <%= t("seed.description_html", name: @name) %>
33
+ #
27
34
  class HtmlTranslationInterpolation < Base
28
35
  MSG = "Translation `%<key>s` contains HTML and interpolates %<variables>s, which `I18n.t` does not " \
29
36
  "escape. Escape each value with `ERB::Util.html_escape`, or keep the markup out of the translation " \
@@ -31,13 +38,19 @@ module RuboCop
31
38
 
32
39
  RESTRICT_ON_SEND = %i[t translate].freeze
33
40
  OPTIONS = %i[locale scope default raise throw count].freeze
41
+ VIEW_FILE = %r{(?:\A|/)app/views/}
34
42
 
35
43
  def_node_matcher :translation_call, <<~PATTERN
36
- (send {nil? (const {nil? cbase} :I18n)} {:t :translate} (str $_) $...)
44
+ (send {nil? (const {nil? cbase} :I18n)} {:t :translate} ${str sym array} $...)
37
45
  PATTERN
38
46
 
39
47
  def_node_matcher :escaped_value?, <<~PATTERN
40
- (send {nil? (const (const {nil? cbase} :ERB) :Util)} {:html_escape :h} _)
48
+ {
49
+ (send {nil? (const (const {nil? cbase} :ERB) :Util)} {:html_escape :h} _)
50
+ (send (const {nil? cbase} :CGI) :escapeHTML _)
51
+ (send _ {:link_to :content_tag :mail_to :safe_join} ...)
52
+ (send (send _ :tag) _ ...)
53
+ }
41
54
  PATTERN
42
55
 
43
56
  def self.translations_cache
@@ -45,10 +58,13 @@ module RuboCop
45
58
  end
46
59
 
47
60
  def on_send(node)
48
- translation_call(node) do |key, arguments|
61
+ translation_call(node) do |key_node, arguments|
49
62
  options = arguments.last
50
63
  next unless options&.hash_type?
51
64
 
65
+ key = translation_key(key_node, options)
66
+ next if key.nil? || escaped_by_view_helper?(node, key)
67
+
52
68
  translation = translations[key]
53
69
  next unless translation.is_a?(String) && HtmlInStringLiteral::HTML_TAG.match?(translation)
54
70
 
@@ -61,6 +77,27 @@ module RuboCop
61
77
 
62
78
  private
63
79
 
80
+ def escaped_by_view_helper?(node, key)
81
+ node.receiver.nil? && VIEW_FILE.match?(processed_source.file_path.to_s) &&
82
+ (key.end_with?("_html") || key.split(".").last == "html")
83
+ end
84
+
85
+ def translation_key(key_node, options)
86
+ key = literal_path(key_node)
87
+ scope = options.pairs.find { |pair| pair.key.sym_type? && pair.key.value == :scope }&.value
88
+ return key if scope.nil? || key.nil?
89
+
90
+ scope_path = literal_path(scope)
91
+ "#{scope_path}.#{key}" if scope_path
92
+ end
93
+
94
+ def literal_path(node)
95
+ parts = node.array_type? ? node.values : [node]
96
+ return unless parts.all? { |part| %i[str sym].include?(part.type) }
97
+
98
+ parts.map { |part| part.value.to_s }.join(".")
99
+ end
100
+
64
101
  def unescaped_variables(options)
65
102
  options.pairs.filter_map do |pair|
66
103
  next unless pair.key.sym_type?
@@ -72,14 +109,14 @@ module RuboCop
72
109
 
73
110
  def translations
74
111
  self.class.translations_cache[locale_files] ||= locale_files.each_with_object({}) do |path, result|
75
- next unless File.exist?(path)
76
-
77
112
  YAML.safe_load_file(path, aliases: true)&.each_value { |tree| flatten(tree, nil, result) }
78
113
  end
79
114
  end
80
115
 
81
116
  def locale_files
82
- Array(cop_config["LocaleFiles"]).map { |path| File.expand_path(path, config.base_dir_for_path_parameters) }
117
+ Array(cop_config["LocaleFiles"]).flat_map do |pattern|
118
+ Dir.glob(File.expand_path(pattern, config.base_dir_for_path_parameters))
119
+ end.sort
83
120
  end
84
121
 
85
122
  def flatten(tree, prefix, result)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Neeto
5
- VERSION = "0.1.20"
5
+ VERSION = "0.1.21"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-neeto
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.20
4
+ version: 0.1.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Neeraj Singh
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-12 00:00:00.000000000 Z
11
+ date: 2026-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop