gl_rubocop 0.5.3 → 0.5.4

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: 1047a8465eb718c62b4582a7c18f339e17acb15bf0861c60460d21f2f1d74a65
4
- data.tar.gz: c85258ff5eb9526fc05b9eb6d8dd15aa8b63f94fe868ed434356259080153fc8
3
+ metadata.gz: cbecf0a138a66001d115607b6e635bda71eb4a2637c7eaa86b164807313efc8c
4
+ data.tar.gz: a40a64ea1547488a822ed7f021c688da5187ec57dde140357d05b3f624c9bbfb
5
5
  SHA512:
6
- metadata.gz: 39d62d81dfbb69a9e213efb8df7f02486906849e9218572d3a48a76758715e3f598c14bbbcce9abfd483edc96f32f8d9dcb2167c44085774bd3e71573ac552a9
7
- data.tar.gz: c32264166f4d1ba98cd73606541671718308de01b47e38c9b08cc59d9aa4496f1ea9a33385807e1aabe9a129e2c920e0433266107588028ad038c98b83e595af
6
+ metadata.gz: 25d837867c4e28a5e2e7134db085a93b94392ceb783bd533240c04a9f582adbfc60033edd5fadce6b5bfa5ebf569728fd258b2394a8f4dff6c9128df44fca953
7
+ data.tar.gz: 6af51558e77b72d5f791f51a5cb87f4a4fa51c953d08b69f9a4c1d87b6724cf5f9601a94e9b8f47f0ee6d06618a53e6c8bb50774cdd5796ddaebc59f98aafe4e
data/default.yml CHANGED
@@ -25,6 +25,9 @@ require:
25
25
  - ./lib/gl_rubocop/gl_cops/view_component_class_naming.rb
26
26
  - ./lib/gl_rubocop/gl_cops/view_component_inheritance.rb
27
27
  - ./lib/gl_rubocop/gl_cops/view_component_directory_structure.rb
28
+ - ./lib/gl_rubocop/gl_cops/no_hardcoded_strings_in_erb_text_elements.rb
29
+ - ./lib/gl_rubocop/gl_cops/text_and_content_variable_naming.rb
30
+ - ./lib/gl_rubocop/gl_cops/no_hardcoded_string_assignment_to_text_or_content_variable.rb
28
31
 
29
32
  AllCops:
30
33
  SuggestExtensions: false
@@ -63,6 +66,16 @@ GLCops/InteractorInheritsFromInteractorBase:
63
66
  GLCops/LimitFlashOptions:
64
67
  Enabled: true
65
68
 
69
+ GLCops/NoHardcodedStringAssignmentToTextOrContentVariable:
70
+ Enabled: true
71
+ Include:
72
+ - "app/components/**/*.rb"
73
+
74
+ GLCops/NoHardcodedStringsInErbTextElements:
75
+ Enabled: true
76
+ Include:
77
+ - "app/components/**/*.erb"
78
+
66
79
  GLCops/NoStubbingEnv:
67
80
  Enabled: true
68
81
  Include:
@@ -88,6 +101,11 @@ GLCops/TailwindNoContradictingClassName:
88
101
  # Disabling the tailwind no contradicting class name for some fixes
89
102
  Enabled: false
90
103
 
104
+ GLCops/TextAndContentVariableNaming:
105
+ Enabled: true
106
+ Include:
107
+ - "app/components/**/*.erb"
108
+
91
109
  GLCops/UniqueIdentifier:
92
110
  Enabled: true
93
111
  Include:
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GLRubocop
4
+ module GLCops
5
+ # Ensures that variables and parameters named with _text, text, _content,
6
+ # or content suffixes are never assigned a hardcoded string literal. Create an
7
+ # i18n entry and use t() or I18n.t() instead.
8
+ #
9
+ # Good:
10
+ # title_text = t('components.title')
11
+ # @label_text = I18n.t('components.label')
12
+ # def initialize(button_text: t('components.button.default'))
13
+ # title_text = content_tag(:span, t('components.title'))
14
+ # @label_content = tag.p(t('components.label'), class: 'label')
15
+ #
16
+ # Bad:
17
+ # title_text = 'Hello'
18
+ # @label_text = 'Click here'
19
+ # def initialize(button_text: 'Submit')
20
+ class NoHardcodedStringAssignmentToTextOrContentVariable < RuboCop::Cop::Cop
21
+ MSG = '`%<name>s` must not be assigned a hardcoded string. ' \
22
+ 'Create an i18n entry and use t() instead.'
23
+
24
+ def on_lvasgn(node)
25
+ name, value = node.children
26
+ return unless text_or_content_name?(name)
27
+ return unless value&.str_type?
28
+
29
+ add_offense(value, message: format(MSG, name:))
30
+ end
31
+
32
+ def on_ivasgn(node)
33
+ name, value = node.children
34
+ return unless text_or_content_name?(name.to_s.delete_prefix('@'))
35
+ return unless value&.str_type?
36
+
37
+ add_offense(value, message: format(MSG, name:))
38
+ end
39
+
40
+ def on_kwoptarg(node)
41
+ name, default = node.children
42
+ return unless text_or_content_name?(name)
43
+ return unless default&.str_type?
44
+
45
+ add_offense(default, message: format(MSG, name:))
46
+ end
47
+
48
+ private
49
+
50
+ def text_or_content_name?(name)
51
+ n = name.to_s
52
+ n == 'text' || n == 'content' || n.end_with?('_text', '_content')
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helpers/erb_content_helper'
4
+
5
+ module GLRubocop
6
+ module GLCops
7
+ # Ensures that text-containing HTML elements in ERB files do not have
8
+ # literal text outside of ERB expressions. All visible text must be
9
+ # wrapped in <%= t() %> or a variable expression.
10
+ #
11
+ # Good:
12
+ # <p><%= t('components.message') %></p>
13
+ # <span><%= @label_text %></span>
14
+ # <h1><%= @title_text %></h1>
15
+ # <%= content_tag(:p, t('components.message')) %>
16
+ #
17
+ # Bad:
18
+ # <p>Hello World</p>
19
+ # <span>Click here</span>
20
+ # <h1>Page Title</h1>
21
+ class NoHardcodedStringsInErbTextElements < RuboCop::Cop::Cop
22
+ include GLRubocop::ErbContentHelper
23
+
24
+ MSG = 'Hardcoded string in <%<tag>s> on line %<line>d. ' \
25
+ 'Use an i18n expression (t()) or a variable.'
26
+
27
+ TEXT_TAGS = %w[
28
+ a span strong em b i p
29
+ h1 h2 h3 h4 h5 h6
30
+ blockquote li td th
31
+ label button dt dd caption
32
+ ].freeze
33
+
34
+ def investigate(processed_source)
35
+ return unless erb_file?
36
+
37
+ content = read_erb_file
38
+ return unless content
39
+
40
+ find_and_report_hardcoded_strings(content, processed_source)
41
+ end
42
+
43
+ private
44
+
45
+ def find_and_report_hardcoded_strings(content, processed_source)
46
+ TEXT_TAGS.each do |tag|
47
+ pattern = element_pattern(tag)
48
+ content.scan(pattern) do |groups|
49
+ inner = groups[0]
50
+ next unless hardcoded_text?(inner)
51
+
52
+ match_start = Regexp.last_match.begin(0)
53
+ line_number = content[0...match_start].count("\n") + 1
54
+ range = processed_source.buffer.source_range
55
+ add_offense(nil, location: range, message: format(MSG, tag:, line: line_number))
56
+ end
57
+ end
58
+ end
59
+
60
+ # Matches <tag ...attrs...>body</tag> where attrs may include ERB expressions.
61
+ def element_pattern(tag)
62
+ Regexp.new(
63
+ "<#{Regexp.escape(tag)}\\b(?:[^>]|<%.*?%>)*>(.*?)<\\/#{Regexp.escape(tag)}>",
64
+ Regexp::MULTILINE | Regexp::IGNORECASE
65
+ )
66
+ end
67
+
68
+ def hardcoded_text?(inner_content)
69
+ # Skip elements that contain child HTML tags — only flag leaf-level text
70
+ # to avoid duplicate offenses on nested elements.
71
+ return false if inner_content.match?(/<[a-z]/i)
72
+
73
+ stripped = inner_content.gsub(/<%.*?%>/m, '')
74
+ stripped.match?(/[[:alpha:]]/)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helpers/erb_content_helper'
4
+
5
+ module GLRubocop
6
+ module GLCops
7
+ # Ensures that simple variable references inside text-containing HTML
8
+ # elements in ERB files are named with a _text or _content suffix (or
9
+ # are exactly `text` or `content`). This makes the intent explicit:
10
+ # _text for plain strings, _content for text-or-HTML values.
11
+ #
12
+ # Configuration parameters that are not rendered as user-visible text
13
+ # (e.g. variant, size, href) belong in non-text elements and are exempt.
14
+ #
15
+ # Good:
16
+ # <p><%= @message_text %></p>
17
+ # <span><%= @banner_content %></span>
18
+ # <h1><%= text %></h1>
19
+ # <p class="<%= @variant %>"><%= @label_text %></p> (variant is in attribute, not body)
20
+ #
21
+ # Bad:
22
+ # <p><%= @message %></p>
23
+ # <span><%= @title %></span>
24
+ class TextAndContentVariableNaming < RuboCop::Cop::Cop
25
+ include GLRubocop::ErbContentHelper
26
+
27
+ MSG = '`%<name>s` (line %<line>d) is rendered inside a text element. ' \
28
+ 'Rename it with a `_text` suffix (plain text) or `_content` suffix (HTML content).'
29
+
30
+ TEXT_TAGS = %w[
31
+ a span strong em b i p
32
+ h1 h2 h3 h4 h5 h6
33
+ blockquote li td th
34
+ label button dt dd caption
35
+ ].freeze
36
+
37
+ # Matches only bare variable/ivar references: <%= @name %> or <%= name %>
38
+ # Does NOT match method calls with arguments (t('key'), helper.method, etc.)
39
+ BARE_VAR_PATTERN = /<%=\s*@?(\w+)\s*%>/
40
+
41
+ def investigate(processed_source)
42
+ return unless erb_file?
43
+
44
+ content = read_erb_file
45
+ return unless content
46
+
47
+ find_and_report_improperly_named_variables(content, processed_source)
48
+ end
49
+
50
+ private
51
+
52
+ def find_and_report_improperly_named_variables(content, processed_source)
53
+ TEXT_TAGS.each do |tag|
54
+ pattern = element_pattern(tag)
55
+ content.scan(pattern) do |groups|
56
+ body_start = Regexp.last_match.begin(1)
57
+ check_body_variables(groups[0], body_start, content, processed_source)
58
+ end
59
+ end
60
+ end
61
+
62
+ def check_body_variables(body, body_start, content, processed_source)
63
+ body.scan(BARE_VAR_PATTERN) do |var_match|
64
+ var_name = var_match[0]
65
+ next if properly_named?(var_name)
66
+
67
+ absolute_offset = body_start + Regexp.last_match.begin(0)
68
+ line_number = content[0...absolute_offset].count("\n") + 1
69
+ msg = format(MSG, name: var_name, line: line_number)
70
+ add_offense(nil, location: processed_source.buffer.source_range, message: msg)
71
+ end
72
+ end
73
+
74
+ # Matches <tag ...attrs...>body</tag> where attrs may contain ERB expressions.
75
+ def element_pattern(tag)
76
+ Regexp.new(
77
+ "<#{Regexp.escape(tag)}\\b(?:[^>]|<%.*?%>)*>(.*?)<\\/#{Regexp.escape(tag)}>",
78
+ Regexp::MULTILINE | Regexp::IGNORECASE
79
+ )
80
+ end
81
+
82
+ def properly_named?(name)
83
+ name == 'text' || name == 'content' ||
84
+ name.end_with?('_text', '_content')
85
+ end
86
+ end
87
+ end
88
+ end
@@ -1,3 +1,3 @@
1
1
  module GLRubocop
2
- VERSION = '0.5.3'.freeze
2
+ VERSION = '0.5.4'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gl_rubocop
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.3
4
+ version: 0.5.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Give Lively
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-20 00:00:00.000000000 Z
11
+ date: 2026-06-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -166,12 +166,15 @@ files:
166
166
  - lib/gl_rubocop/gl_cops/consolidate_request_system_specs.rb
167
167
  - lib/gl_rubocop/gl_cops/interactor_inherits_from_interactor_base.rb
168
168
  - lib/gl_rubocop/gl_cops/limit_flash_options.rb
169
+ - lib/gl_rubocop/gl_cops/no_hardcoded_string_assignment_to_text_or_content_variable.rb
170
+ - lib/gl_rubocop/gl_cops/no_hardcoded_strings_in_erb_text_elements.rb
169
171
  - lib/gl_rubocop/gl_cops/no_stubbing_env.rb
170
172
  - lib/gl_rubocop/gl_cops/no_stubbing_perform_async.rb
171
173
  - lib/gl_rubocop/gl_cops/prevent_haml_files.rb
172
174
  - lib/gl_rubocop/gl_cops/rails_cache.rb
173
175
  - lib/gl_rubocop/gl_cops/sidekiq_inherits_from_sidekiq_job.rb
174
176
  - lib/gl_rubocop/gl_cops/tailwind_no_contradicting_class_name.rb
177
+ - lib/gl_rubocop/gl_cops/text_and_content_variable_naming.rb
175
178
  - lib/gl_rubocop/gl_cops/unique_identifier.rb
176
179
  - lib/gl_rubocop/gl_cops/valid_data_test_id.rb
177
180
  - lib/gl_rubocop/gl_cops/vcr_cassette_names.rb