gl_rubocop 0.5.2 → 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 +4 -4
- data/default.yml +24 -0
- data/lib/gl_rubocop/gl_cops/no_hardcoded_string_assignment_to_text_or_content_variable.rb +56 -0
- data/lib/gl_rubocop/gl_cops/no_hardcoded_strings_in_erb_text_elements.rb +78 -0
- data/lib/gl_rubocop/gl_cops/no_stubbing_env.rb +42 -0
- data/lib/gl_rubocop/gl_cops/text_and_content_variable_naming.rb +88 -0
- data/lib/gl_rubocop/version.rb +1 -1
- metadata +7 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cbecf0a138a66001d115607b6e635bda71eb4a2637c7eaa86b164807313efc8c
|
|
4
|
+
data.tar.gz: a40a64ea1547488a822ed7f021c688da5187ec57dde140357d05b3f624c9bbfb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 25d837867c4e28a5e2e7134db085a93b94392ceb783bd533240c04a9f582adbfc60033edd5fadce6b5bfa5ebf569728fd258b2394a8f4dff6c9128df44fca953
|
|
7
|
+
data.tar.gz: 6af51558e77b72d5f791f51a5cb87f4a4fa51c953d08b69f9a4c1d87b6724cf5f9601a94e9b8f47f0ee6d06618a53e6c8bb50774cdd5796ddaebc59f98aafe4e
|
data/default.yml
CHANGED
|
@@ -12,6 +12,7 @@ require:
|
|
|
12
12
|
- ./lib/gl_rubocop/gl_cops/consolidate_request_system_specs.rb
|
|
13
13
|
- ./lib/gl_rubocop/gl_cops/interactor_inherits_from_interactor_base.rb
|
|
14
14
|
- ./lib/gl_rubocop/gl_cops/limit_flash_options.rb
|
|
15
|
+
- ./lib/gl_rubocop/gl_cops/no_stubbing_env.rb
|
|
15
16
|
- ./lib/gl_rubocop/gl_cops/no_stubbing_perform_async.rb
|
|
16
17
|
- ./lib/gl_rubocop/gl_cops/prevent_haml_files.rb
|
|
17
18
|
- ./lib/gl_rubocop/gl_cops/rails_cache.rb
|
|
@@ -24,6 +25,9 @@ require:
|
|
|
24
25
|
- ./lib/gl_rubocop/gl_cops/view_component_class_naming.rb
|
|
25
26
|
- ./lib/gl_rubocop/gl_cops/view_component_inheritance.rb
|
|
26
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
|
|
27
31
|
|
|
28
32
|
AllCops:
|
|
29
33
|
SuggestExtensions: false
|
|
@@ -62,6 +66,21 @@ GLCops/InteractorInheritsFromInteractorBase:
|
|
|
62
66
|
GLCops/LimitFlashOptions:
|
|
63
67
|
Enabled: true
|
|
64
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
|
+
|
|
79
|
+
GLCops/NoStubbingEnv:
|
|
80
|
+
Enabled: true
|
|
81
|
+
Include:
|
|
82
|
+
- "**/*spec.rb"
|
|
83
|
+
|
|
65
84
|
GLCops/NoStubbingPerformAsync:
|
|
66
85
|
Enabled: true
|
|
67
86
|
Include:
|
|
@@ -82,6 +101,11 @@ GLCops/TailwindNoContradictingClassName:
|
|
|
82
101
|
# Disabling the tailwind no contradicting class name for some fixes
|
|
83
102
|
Enabled: false
|
|
84
103
|
|
|
104
|
+
GLCops/TextAndContentVariableNaming:
|
|
105
|
+
Enabled: true
|
|
106
|
+
Include:
|
|
107
|
+
- "app/components/**/*.erb"
|
|
108
|
+
|
|
85
109
|
GLCops/UniqueIdentifier:
|
|
86
110
|
Enabled: true
|
|
87
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,42 @@
|
|
|
1
|
+
module GLRubocop
|
|
2
|
+
module GLCops
|
|
3
|
+
# This cop ensures that you don't stub ENV with `allow`/`expect` + `receive`.
|
|
4
|
+
#
|
|
5
|
+
# Stubbing ENV this way (even with `and_call_original`) only stubs the env vars the
|
|
6
|
+
# spec explicitly knows about. Any var that is read but not stubbed - for example a
|
|
7
|
+
# var that is present on CI but not locally, or vice versa - behaves inconsistently
|
|
8
|
+
# and causes flaky failures.
|
|
9
|
+
#
|
|
10
|
+
# Instead, replace ENV wholesale with `stub_const`, building the hash from the real
|
|
11
|
+
# ENV so unrelated vars keep working everywhere:
|
|
12
|
+
#
|
|
13
|
+
# Good:
|
|
14
|
+
# stub_const('ENV', ENV.to_hash.except('VAR_TO_UNSET').merge('VAR_TO_SET' => 'value'))
|
|
15
|
+
#
|
|
16
|
+
# Bad:
|
|
17
|
+
# allow(ENV).to receive(:[]).and_call_original
|
|
18
|
+
# allow(ENV).to receive(:[]).with('VAR_TO_SET').and_return('value')
|
|
19
|
+
# expect(ENV).to receive(:fetch)
|
|
20
|
+
class NoStubbingEnv < RuboCop::Cop::Base
|
|
21
|
+
MSG = "Don't stub ENV with allow/expect + receive. Use " \
|
|
22
|
+
"stub_const('ENV', ENV.to_hash.except('VAR_TO_UNSET')." \
|
|
23
|
+
"merge('VAR_TO_SET' => 'value')) instead, so unrelated ENV vars " \
|
|
24
|
+
'(e.g. on CI) keep working.'.freeze
|
|
25
|
+
|
|
26
|
+
# Match `allow(ENV).to receive(...)` / `expect(ENV).not_to receive(...)`, including
|
|
27
|
+
# chained forms like `.and_call_original`, `.and_return(...)`, `.with(...)`.
|
|
28
|
+
def_node_matcher :stubbing_env?, <<~PATTERN
|
|
29
|
+
(send
|
|
30
|
+
(send nil? {:allow :expect} (const {nil? cbase} :ENV))
|
|
31
|
+
{:to :not_to :to_not}
|
|
32
|
+
`(send nil? :receive ...))
|
|
33
|
+
PATTERN
|
|
34
|
+
|
|
35
|
+
def on_send(node)
|
|
36
|
+
return unless stubbing_env?(node)
|
|
37
|
+
|
|
38
|
+
add_offense(node)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
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
|
data/lib/gl_rubocop/version.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2026-06-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rubocop
|
|
@@ -166,11 +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
|
|
171
|
+
- lib/gl_rubocop/gl_cops/no_stubbing_env.rb
|
|
169
172
|
- lib/gl_rubocop/gl_cops/no_stubbing_perform_async.rb
|
|
170
173
|
- lib/gl_rubocop/gl_cops/prevent_haml_files.rb
|
|
171
174
|
- lib/gl_rubocop/gl_cops/rails_cache.rb
|
|
172
175
|
- lib/gl_rubocop/gl_cops/sidekiq_inherits_from_sidekiq_job.rb
|
|
173
176
|
- lib/gl_rubocop/gl_cops/tailwind_no_contradicting_class_name.rb
|
|
177
|
+
- lib/gl_rubocop/gl_cops/text_and_content_variable_naming.rb
|
|
174
178
|
- lib/gl_rubocop/gl_cops/unique_identifier.rb
|
|
175
179
|
- lib/gl_rubocop/gl_cops/valid_data_test_id.rb
|
|
176
180
|
- lib/gl_rubocop/gl_cops/vcr_cassette_names.rb
|
|
@@ -201,7 +205,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
201
205
|
- !ruby/object:Gem::Version
|
|
202
206
|
version: '0'
|
|
203
207
|
requirements: []
|
|
204
|
-
rubygems_version: 3.
|
|
208
|
+
rubygems_version: 3.5.22
|
|
205
209
|
signing_key:
|
|
206
210
|
specification_version: 4
|
|
207
211
|
summary: A shareable configuration of Give Lively's rubocop rules.
|