rubocop-neeto 0.1.18 → 0.1.20

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: 0673b79da6f5f0a65905146dc7626d731613ccdf19575ddaa26e4d3d6c32c424
4
- data.tar.gz: e846f0f4339ea9c6496ff54503287480c19464212151f69e846cf6f659e75a57
3
+ metadata.gz: a80cf5441b3f2e8a557f1ab80f1c33f234fd0964051e0c4ba3082ed6ad4ec038
4
+ data.tar.gz: ed16bab4d596c2741d0205e1a3904c9207574564df4481b7aae7fac5541427f1
5
5
  SHA512:
6
- metadata.gz: ba3a84a47cb213fdf8ece57d183ce1b061f14de1fd253ee11d76dd96a5430e659bdb475add57892653c7e7ba5948f91c040211526627ada628fa091de0949194
7
- data.tar.gz: 44f961f81778b30199382f17c53f98847999bf924c814c0bd3f57a51a28250b4ef7ecca8d4b1eeb35cda0d78542369739b02882376fdbb3e9e461ed1480362b9
6
+ metadata.gz: f34434c7395250edfac2c36e73f719bbcf80f37443945e9a6e1fc31958dc1736aba5c3705eeea4f5fd455e72022e2f1e7465ce9810fc72e32a757840aaed0840
7
+ data.tar.gz: 4c4e0f22d50814ceb614509f44218e0733a3466eccb7674d29cc200fed15e9bed465e99c1e8f0d91d2f0a548cccb5c060b3ab65cbf98011e0d7f6aed01f73869
data/README.md CHANGED
@@ -6,6 +6,13 @@
6
6
  2. [Neeto/UnsafeColumnDeletion](https://rubocop-neeto.neetodeployapp.com/docs/RuboCop/Cop/Neeto/UnsafeColumnDeletion)
7
7
  3. [Neeto/DirectEnvAccess](https://rubocop-neeto.neetodeployapp.com/docs/RuboCop/Cop/Neeto/DirectEnvAccess)
8
8
  4. [Neeto/DeprecatedJobBaseClass](https://rubocop-neeto.neetodeployapp.com/docs/RuboCop/Cop/Neeto/DeprecatedJobBaseClass)
9
+ 5. [Neeto/FaradayOverHttparty](https://rubocop-neeto.neetodeployapp.com/docs/RuboCop/Cop/Neeto/FaradayOverHttparty)
10
+ 6. [Neeto/HtmlSafeOnDynamicString](https://rubocop-neeto.neetodeployapp.com/docs/RuboCop/Cop/Neeto/HtmlSafeOnDynamicString)
11
+ 7. [Neeto/HtmlInStringLiteral](https://rubocop-neeto.neetodeployapp.com/docs/RuboCop/Cop/Neeto/HtmlInStringLiteral)
12
+ 8. [Neeto/DirectLiquidRender](https://rubocop-neeto.neetodeployapp.com/docs/RuboCop/Cop/Neeto/DirectLiquidRender)
13
+ 9. [Neeto/HtmlTranslationInterpolation](https://rubocop-neeto.neetodeployapp.com/docs/RuboCop/Cop/Neeto/HtmlTranslationInterpolation)
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.
9
16
 
10
17
  ## Installation
11
18
 
data/config/default.yml CHANGED
@@ -52,3 +52,78 @@ Neeto/DeprecatedJobBaseClass:
52
52
  - app/workers/**/*.rb
53
53
  - test/dummy/app/jobs/**/*.rb
54
54
  - test/dummy/app/workers/**/*.rb
55
+
56
+ Neeto/FaradayOverHttparty:
57
+ Description: >-
58
+ HTTParty has been removed from all Neeto applications in favor of Faraday,
59
+ which is already available. Do not re-add the `httparty` gem. See
60
+ https://github.com/neetozone/neeto-engineering/issues/1862 for more details.
61
+ Enabled: true
62
+ Severity: refactor
63
+ VersionAdded: '0.1'
64
+ Include:
65
+ - '**/Gemfile'
66
+ - '**/Gemfile.common.rb'
67
+
68
+ Neeto/HtmlSafeOnDynamicString:
69
+ Description: >-
70
+ `html_safe`, `raw` and `safe_concat` mark a string as already safe, so
71
+ Rails emits it without escaping. Calling them on dynamic values
72
+ (interpolation, method results, variables) turns off output escaping for
73
+ that value. Build markup with `tag`, `content_tag`, `link_to`, `mail_to`
74
+ or `safe_join` instead, or annotate a reviewed call with
75
+ `# neeto:html-safe-reviewed <reason>`.
76
+ Enabled: true
77
+ Severity: info
78
+ VersionAdded: '0.1'
79
+ Include:
80
+ - app/**/*.rb
81
+ - lib/**/*.rb
82
+ - app/views/**/*.erb
83
+ - app/views/**/*.inky
84
+
85
+ Neeto/HtmlInStringLiteral:
86
+ Description: >-
87
+ Plain Ruby strings never escape what is interpolated into them. Build
88
+ markup outside views with `tag`, `content_tag`, `link_to`, `mail_to`,
89
+ `safe_join` or a partial instead of string literals and heredocs.
90
+ Enabled: true
91
+ Severity: info
92
+ VersionAdded: '0.1'
93
+ Include:
94
+ - app/**/*.rb
95
+ - lib/**/*.rb
96
+ Exclude:
97
+ - app/views/**/*
98
+ - lib/tasks/**/*
99
+
100
+ Neeto/DirectLiquidRender:
101
+ Description: >-
102
+ `Liquid::Template#render` does not escape its output. Build and render
103
+ Liquid templates only through `NeetoCommonsBackend::LiquidRenderer`, which
104
+ escapes every output and requires an explicit format (`:html` or `:text`).
105
+ Check a template's syntax with `NeetoCommonsBackend::LiquidRenderer.validate!`.
106
+ Enabled: true
107
+ Severity: info
108
+ VersionAdded: '0.1'
109
+ Include:
110
+ - app/**/*.rb
111
+ - lib/**/*.rb
112
+
113
+ Neeto/HtmlTranslationInterpolation:
114
+ Description: >-
115
+ `I18n.t` does not escape interpolated values, even for `_html` keys. A
116
+ translation that contains HTML must not interpolate unescaped values.
117
+ Escape each value with `ERB::Util.html_escape`, or keep the markup out of
118
+ the translation and build it with `tag` or `content_tag`.
119
+ Enabled: true
120
+ Severity: info
121
+ VersionAdded: '0.1'
122
+ LocaleFiles:
123
+ - config/locales/en.yml
124
+ Include:
125
+ - app/**/*.rb
126
+ - lib/**/*.rb
127
+ Exclude:
128
+ - app/views/**/*
129
+ - lib/tasks/**/*
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Neeto
6
+ # `Liquid::Template#render` does not escape its output, so every variable
7
+ # reaches the page or email as-is. Render Liquid through the shared
8
+ # renderer, which escapes every output and requires an explicit format.
9
+ # Code that only checks a template's syntax uses `validate!`, which raises
10
+ # `Liquid::SyntaxError` like `Liquid::Template.parse` does.
11
+ #
12
+ # @example DirectLiquidRender: true (default)
13
+ # # bad
14
+ # Liquid::Template.parse(template.body).render(variables)
15
+ #
16
+ # # good
17
+ # NeetoCommonsBackend::LiquidRenderer.render(template.body, variables, format: :html)
18
+ #
19
+ # # good
20
+ # NeetoCommonsBackend::LiquidRenderer.render(template.subject, variables, format: :text)
21
+ #
22
+ # # bad
23
+ # Liquid::Template.parse(metadata["body"])
24
+ #
25
+ # # good
26
+ # NeetoCommonsBackend::LiquidRenderer.validate!(metadata["body"])
27
+ #
28
+ class DirectLiquidRender < Base
29
+ MSG = "Do not use `Liquid::Template.%<method>s` directly. Render with " \
30
+ "`NeetoCommonsBackend::LiquidRenderer.render(template, variables, format: :html)`, which escapes " \
31
+ "every output, or check syntax with `NeetoCommonsBackend::LiquidRenderer.validate!(template)`."
32
+
33
+ RESTRICT_ON_SEND = %i[parse new].freeze
34
+
35
+ def_node_matcher :liquid_template_build?, <<~PATTERN
36
+ (send (const (const {nil? cbase} :Liquid) :Template) {:parse :new} ...)
37
+ PATTERN
38
+
39
+ def on_send(node)
40
+ return unless liquid_template_build?(node)
41
+
42
+ add_offense(node, message: format(MSG, method: node.method_name))
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Neeto
6
+ # HTTParty has been removed from all Neeto applications in favor of
7
+ # Faraday, which is already available. This cop flags any attempt to add
8
+ # the `httparty` gem to a Gemfile.
9
+ #
10
+ # @example FaradayOverHttparty: true (default)
11
+ # # Disallows adding the `httparty` gem.
12
+ #
13
+ # # bad
14
+ # gem "httparty"
15
+ #
16
+ # # good
17
+ # gem "faraday"
18
+ #
19
+ class FaradayOverHttparty < Base
20
+ MSG = "Do not add the `httparty` gem. Use `faraday` instead, which is " \
21
+ "already available in the application. See " \
22
+ "https://github.com/neetozone/neeto-engineering/issues/1862 for more details."
23
+
24
+ RESTRICT_ON_SEND = %i[gem].freeze
25
+
26
+ def_node_matcher :httparty_gem?, <<~PATTERN
27
+ (send nil? :gem (str $_) ...)
28
+ PATTERN
29
+
30
+ def on_send(node)
31
+ httparty_gem?(node) do |gem_name|
32
+ add_offense(node) if gem_name.casecmp?("httparty")
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Neeto
6
+ # Ruby strings never escape what is interpolated into them, so HTML built
7
+ # in a string or heredoc outside views is one missed escape away from an
8
+ # injection. Build markup with `tag`, `content_tag`, `link_to`,
9
+ # `mail_to`, `safe_join` or a partial, which escape their arguments.
10
+ #
11
+ # @example HtmlInStringLiteral: true (default)
12
+ # # bad
13
+ # "<a href='#{url}'>#{name}</a>"
14
+ #
15
+ # # bad
16
+ # <<~HTML
17
+ # <p>#{description}</p>
18
+ # HTML
19
+ #
20
+ # # good
21
+ # link_to(name, url)
22
+ #
23
+ # # good
24
+ # tag.p(description)
25
+ #
26
+ # # good (searching for markup is not building it)
27
+ # body.include?("<table")
28
+ #
29
+ class HtmlInStringLiteral < Base
30
+ MSG = "Do not build HTML in Ruby strings. Use `tag`, `content_tag`, `link_to`, `mail_to`, `safe_join` " \
31
+ "or a partial, which escape their arguments."
32
+
33
+ HTML_TAG = %r{<(?:[a-z][a-z0-9]*[\s/>]|/[a-z][a-z0-9]*\s*>)}i
34
+ FORMAT_REFERENCE = /%<\w+>/
35
+
36
+ ENCLOSING_LITERAL_TYPES = %i[dstr dsym xstr regexp].freeze
37
+
38
+ SEARCH_METHODS = %i[
39
+ include? start_with? end_with? index rindex match match? split scan count delete
40
+ gsub gsub! sub sub! == != eql?
41
+ ].freeze
42
+
43
+ def on_str(node)
44
+ return if part_of_another_literal?(node)
45
+
46
+ check(node, node.value)
47
+ end
48
+
49
+ def on_dstr(node)
50
+ return if part_of_another_literal?(node)
51
+
52
+ check(node, node.each_child_node(:str).map(&:value).join)
53
+ end
54
+
55
+ private
56
+
57
+ def check(node, value)
58
+ return unless value.valid_encoding? && HTML_TAG.match?(value.gsub(FORMAT_REFERENCE, ""))
59
+ return if search_argument?(node)
60
+
61
+ add_offense(node)
62
+ end
63
+
64
+ def part_of_another_literal?(node)
65
+ ENCLOSING_LITERAL_TYPES.include?(node.parent&.type)
66
+ end
67
+
68
+ def search_argument?(node)
69
+ parent = node.parent
70
+ return false unless parent&.call_type? && SEARCH_METHODS.include?(parent.method_name)
71
+
72
+ parent.first_argument.equal?(node) || (parent.comparison_method? && parent.receiver.equal?(node))
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Neeto
6
+ # `html_safe`, `raw` and `safe_concat` mark a string as already safe, so
7
+ # Rails prints it without escaping. Calling them on a dynamic value
8
+ # (interpolation, a method result, a variable) turns escaping off for
9
+ # whatever that value holds, which lets user input inject markup. Build
10
+ # markup with `tag`, `content_tag`, `link_to`, `mail_to` or `safe_join`
11
+ # instead. A call that has been reviewed can be kept by adding
12
+ # `# neeto:html-safe-reviewed <reason>` on or above it.
13
+ #
14
+ # @example HtmlSafeOnDynamicString: true (default)
15
+ # # bad
16
+ # "<b>#{booking.name}</b>".html_safe
17
+ #
18
+ # # bad
19
+ # raw(message.body)
20
+ #
21
+ # # good
22
+ # tag.b(booking.name)
23
+ #
24
+ # # good
25
+ # "<br>".html_safe
26
+ #
27
+ # # good
28
+ # # neeto:html-safe-reviewed admin-authored rich text
29
+ # meeting.cancellation_policy.html_safe
30
+ #
31
+ class HtmlSafeOnDynamicString < Base
32
+ MSG = "Do not call `%<method>s` on a dynamic value. Build the markup with `tag`, `content_tag`, " \
33
+ "`link_to`, `mail_to` or `safe_join`, or add `# neeto:html-safe-reviewed <reason>` if the value " \
34
+ "is known to be safe."
35
+
36
+ REVIEWED_ANNOTATION = /#\s*neeto:html-safe-reviewed\s+\S/
37
+
38
+ RESTRICT_ON_SEND = %i[html_safe raw safe_concat].freeze
39
+
40
+ def on_send(node)
41
+ value = trusted_value(node)
42
+ return if value.nil? || static_string?(value) || reviewed?(node)
43
+
44
+ add_offense(node.loc.selector, message: format(MSG, method: node.method_name))
45
+ end
46
+ alias on_csend on_send
47
+
48
+ private
49
+
50
+ def trusted_value(node)
51
+ case node.method_name
52
+ when :html_safe
53
+ node.receiver if node.arguments.none?
54
+ when :raw
55
+ node.first_argument if node.receiver.nil? && node.arguments.one?
56
+ when :safe_concat
57
+ node.first_argument if node.arguments.one?
58
+ end
59
+ end
60
+
61
+ def static_string?(node)
62
+ node.str_type? || (node.dstr_type? && node.children.all? { |child| static_string?(child) })
63
+ end
64
+
65
+ def reviewed?(node)
66
+ lines = (node.first_line - 1)..node.last_line
67
+
68
+ processed_source.comments.any? do |comment|
69
+ lines.cover?(comment.loc.line) && REVIEWED_ANNOTATION.match?(comment.text)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Neeto
8
+ # `I18n.t` does not escape interpolated values, even for `_html` keys
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`.
12
+ #
13
+ # @example HtmlTranslationInterpolation: true (default)
14
+ # # en.yml
15
+ # # seed:
16
+ # # description: "<p>Schedule a meeting with %{name}</p>"
17
+ #
18
+ # # bad
19
+ # I18n.t("seed.description", name: user.name)
20
+ #
21
+ # # good
22
+ # I18n.t("seed.description", name: ERB::Util.html_escape(user.name))
23
+ #
24
+ # # good
25
+ # tag.p(I18n.t("seed.description_text", name: user.name))
26
+ #
27
+ class HtmlTranslationInterpolation < Base
28
+ MSG = "Translation `%<key>s` contains HTML and interpolates %<variables>s, which `I18n.t` does not " \
29
+ "escape. Escape each value with `ERB::Util.html_escape`, or keep the markup out of the translation " \
30
+ "and build it with `tag` or `content_tag`."
31
+
32
+ RESTRICT_ON_SEND = %i[t translate].freeze
33
+ OPTIONS = %i[locale scope default raise throw count].freeze
34
+
35
+ def_node_matcher :translation_call, <<~PATTERN
36
+ (send {nil? (const {nil? cbase} :I18n)} {:t :translate} (str $_) $...)
37
+ PATTERN
38
+
39
+ def_node_matcher :escaped_value?, <<~PATTERN
40
+ (send {nil? (const (const {nil? cbase} :ERB) :Util)} {:html_escape :h} _)
41
+ PATTERN
42
+
43
+ def self.translations_cache
44
+ @_translations_cache ||= {}
45
+ end
46
+
47
+ def on_send(node)
48
+ translation_call(node) do |key, arguments|
49
+ options = arguments.last
50
+ next unless options&.hash_type?
51
+
52
+ translation = translations[key]
53
+ next unless translation.is_a?(String) && HtmlInStringLiteral::HTML_TAG.match?(translation)
54
+
55
+ variables = unescaped_variables(options).select { |name| translation.include?("%{#{name}}") }
56
+ next if variables.empty?
57
+
58
+ add_offense(node, message: format(MSG, key:, variables: variables.map { |name| "%{#{name}}" }.join(", ")))
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def unescaped_variables(options)
65
+ options.pairs.filter_map do |pair|
66
+ next unless pair.key.sym_type?
67
+ next if OPTIONS.include?(pair.key.value) || escaped_value?(pair.value)
68
+
69
+ pair.key.value.to_s
70
+ end
71
+ end
72
+
73
+ def translations
74
+ self.class.translations_cache[locale_files] ||= locale_files.each_with_object({}) do |path, result|
75
+ next unless File.exist?(path)
76
+
77
+ YAML.safe_load_file(path, aliases: true)&.each_value { |tree| flatten(tree, nil, result) }
78
+ end
79
+ end
80
+
81
+ def locale_files
82
+ Array(cop_config["LocaleFiles"]).map { |path| File.expand_path(path, config.base_dir_for_path_parameters) }
83
+ end
84
+
85
+ def flatten(tree, prefix, result)
86
+ return result[prefix] = tree unless tree.is_a?(Hash)
87
+
88
+ tree.each { |key, value| flatten(value, [prefix, key].compact.join("."), result) }
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -4,3 +4,8 @@ require_relative "neeto/unsafe_table_deletion"
4
4
  require_relative "neeto/unsafe_column_deletion"
5
5
  require_relative "neeto/direct_env_access"
6
6
  require_relative "neeto/deprecated_job_base_class"
7
+ require_relative "neeto/faraday_over_httparty"
8
+ require_relative "neeto/html_safe_on_dynamic_string"
9
+ require_relative "neeto/html_in_string_literal"
10
+ require_relative "neeto/direct_liquid_render"
11
+ require_relative "neeto/html_translation_interpolation"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Neeto
5
- VERSION = "0.1.18"
5
+ VERSION = "0.1.20"
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.18
4
+ version: 0.1.20
5
5
  platform: ruby
6
6
  authors:
7
- - Yedhin Kizhakkethara
7
+ - Neeraj Singh
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-22 00:00:00.000000000 Z
11
+ date: 2026-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -26,7 +26,7 @@ dependencies:
26
26
  version: '0'
27
27
  description:
28
28
  email:
29
- - yedhin@bigbinary.com
29
+ - neeraj@neeto.com
30
30
  executables: []
31
31
  extensions: []
32
32
  extra_rdoc_files: []
@@ -37,6 +37,11 @@ files:
37
37
  - lib/rubocop-neeto.rb
38
38
  - lib/rubocop/cop/neeto/deprecated_job_base_class.rb
39
39
  - lib/rubocop/cop/neeto/direct_env_access.rb
40
+ - lib/rubocop/cop/neeto/direct_liquid_render.rb
41
+ - lib/rubocop/cop/neeto/faraday_over_httparty.rb
42
+ - lib/rubocop/cop/neeto/html_in_string_literal.rb
43
+ - lib/rubocop/cop/neeto/html_safe_on_dynamic_string.rb
44
+ - lib/rubocop/cop/neeto/html_translation_interpolation.rb
40
45
  - lib/rubocop/cop/neeto/unsafe_column_deletion.rb
41
46
  - lib/rubocop/cop/neeto/unsafe_table_deletion.rb
42
47
  - lib/rubocop/cop/neeto_cops.rb