rubocop-view_component 0.1.0
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +7 -0
- data/IMPLEMENTATION_SUMMARY.md +172 -0
- data/LICENSE.txt +21 -0
- data/PLAN.md +625 -0
- data/README.md +53 -0
- data/Rakefile +35 -0
- data/config/default.yml +33 -0
- data/lib/rubocop/cop/view_component/base.rb +41 -0
- data/lib/rubocop/cop/view_component/component_suffix.rb +33 -0
- data/lib/rubocop/cop/view_component/no_global_state.rb +58 -0
- data/lib/rubocop/cop/view_component/prefer_private_methods.rb +73 -0
- data/lib/rubocop/cop/view_component/prefer_slots.rb +105 -0
- data/lib/rubocop/cop/view_component_cops.rb +7 -0
- data/lib/rubocop/view_component/plugin.rb +31 -0
- data/lib/rubocop/view_component/version.rb +7 -0
- data/lib/rubocop/view_component.rb +10 -0
- data/lib/rubocop-view_component.rb +9 -0
- data/spec/rubocop/cop/view_component/component_suffix_spec.rb +86 -0
- data/spec/rubocop/cop/view_component/no_global_state_spec.rb +119 -0
- data/spec/rubocop/cop/view_component/prefer_private_methods_spec.rb +136 -0
- data/spec/rubocop/cop/view_component/prefer_slots_spec.rb +119 -0
- data/spec/spec_helper.rb +14 -0
- metadata +108 -0
data/config/default.yml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
ViewComponent/ComponentSuffix:
|
|
2
|
+
Description: 'Enforce -Component suffix for ViewComponent classes.'
|
|
3
|
+
Enabled: true
|
|
4
|
+
VersionAdded: '0.1'
|
|
5
|
+
Severity: warning
|
|
6
|
+
StyleGuide: 'https://viewcomponent.org/best_practices.html'
|
|
7
|
+
|
|
8
|
+
ViewComponent/NoGlobalState:
|
|
9
|
+
Description: 'Avoid accessing global state (params, request, session, cookies, flash) directly.'
|
|
10
|
+
Enabled: true
|
|
11
|
+
VersionAdded: '0.1'
|
|
12
|
+
Severity: warning
|
|
13
|
+
StyleGuide: 'https://viewcomponent.org/best_practices.html'
|
|
14
|
+
|
|
15
|
+
ViewComponent/PreferPrivateMethods:
|
|
16
|
+
Description: 'Suggest making helper methods private.'
|
|
17
|
+
Enabled: true
|
|
18
|
+
VersionAdded: '0.1'
|
|
19
|
+
Severity: convention
|
|
20
|
+
StyleGuide: 'https://viewcomponent.org/best_practices.html'
|
|
21
|
+
AllowedPublicMethods:
|
|
22
|
+
- initialize
|
|
23
|
+
- call
|
|
24
|
+
- before_render
|
|
25
|
+
- before_render_check
|
|
26
|
+
- render?
|
|
27
|
+
|
|
28
|
+
ViewComponent/PreferSlots:
|
|
29
|
+
Description: 'Prefer slots over HTML string parameters.'
|
|
30
|
+
Enabled: true
|
|
31
|
+
VersionAdded: '0.1'
|
|
32
|
+
Severity: warning
|
|
33
|
+
StyleGuide: 'https://viewcomponent.org/best_practices.html'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module ViewComponent
|
|
6
|
+
# Shared helper methods for ViewComponent cops
|
|
7
|
+
module Base
|
|
8
|
+
# Check if a class node inherits from ViewComponent::Base or ApplicationComponent
|
|
9
|
+
def view_component_class?(node)
|
|
10
|
+
return false unless node&.class_type?
|
|
11
|
+
|
|
12
|
+
parent_class = node.parent_class
|
|
13
|
+
return false unless parent_class
|
|
14
|
+
|
|
15
|
+
view_component_parent?(parent_class)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Check if node represents ViewComponent::Base or ApplicationComponent
|
|
19
|
+
def view_component_parent?(node)
|
|
20
|
+
return false unless node.const_type?
|
|
21
|
+
|
|
22
|
+
source = node.source
|
|
23
|
+
source == "ViewComponent::Base" || source == "ApplicationComponent"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Find the enclosing class node
|
|
27
|
+
def enclosing_class(node)
|
|
28
|
+
node.each_ancestor(:class).first
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Check if node is within a ViewComponent class
|
|
32
|
+
def inside_view_component?(node)
|
|
33
|
+
klass = enclosing_class(node)
|
|
34
|
+
return false unless klass
|
|
35
|
+
|
|
36
|
+
view_component_class?(klass)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module ViewComponent
|
|
6
|
+
# Enforces that ViewComponent classes end with the `Component` suffix.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# class FooBar < ViewComponent::Base
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# # good
|
|
14
|
+
# class FooBarComponent < ViewComponent::Base
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
class ComponentSuffix < RuboCop::Cop::Base
|
|
18
|
+
include ViewComponent::Base
|
|
19
|
+
|
|
20
|
+
MSG = "ViewComponent class names should end with `Component`."
|
|
21
|
+
|
|
22
|
+
def on_class(node)
|
|
23
|
+
return unless view_component_class?(node)
|
|
24
|
+
|
|
25
|
+
class_name = node.identifier.source
|
|
26
|
+
return if class_name.end_with?("Component")
|
|
27
|
+
|
|
28
|
+
add_offense(node.identifier)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module ViewComponent
|
|
6
|
+
# Prevents direct access to global state within ViewComponent classes.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# class UserComponent < ViewComponent::Base
|
|
11
|
+
# def admin?
|
|
12
|
+
# params[:admin]
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# # good
|
|
17
|
+
# class UserComponent < ViewComponent::Base
|
|
18
|
+
# def initialize(admin:)
|
|
19
|
+
# @admin = admin
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# def admin?
|
|
23
|
+
# @admin
|
|
24
|
+
# end
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
class NoGlobalState < RuboCop::Cop::Base
|
|
28
|
+
include ViewComponent::Base
|
|
29
|
+
|
|
30
|
+
MSG = "Avoid accessing `%<method>s` directly in ViewComponents. " \
|
|
31
|
+
"Pass necessary data through the constructor instead."
|
|
32
|
+
|
|
33
|
+
GLOBAL_STATE_METHODS = %i[
|
|
34
|
+
params
|
|
35
|
+
request
|
|
36
|
+
session
|
|
37
|
+
cookies
|
|
38
|
+
flash
|
|
39
|
+
].freeze
|
|
40
|
+
|
|
41
|
+
RESTRICT_ON_SEND = GLOBAL_STATE_METHODS
|
|
42
|
+
|
|
43
|
+
def_node_matcher :global_state_access?, <<~PATTERN
|
|
44
|
+
(send nil? ${:params :request :session :cookies :flash} ...)
|
|
45
|
+
PATTERN
|
|
46
|
+
|
|
47
|
+
def on_send(node)
|
|
48
|
+
return unless inside_view_component?(node)
|
|
49
|
+
|
|
50
|
+
method_name = global_state_access?(node)
|
|
51
|
+
return unless method_name
|
|
52
|
+
|
|
53
|
+
add_offense(node, message: format(MSG, method: method_name))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module ViewComponent
|
|
6
|
+
# Suggests making helper methods private in ViewComponents.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# class CardComponent < ViewComponent::Base
|
|
11
|
+
# def formatted_title
|
|
12
|
+
# @title.upcase
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# # good
|
|
17
|
+
# class CardComponent < ViewComponent::Base
|
|
18
|
+
# private
|
|
19
|
+
#
|
|
20
|
+
# def formatted_title
|
|
21
|
+
# @title.upcase
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
class PreferPrivateMethods < RuboCop::Cop::Base
|
|
26
|
+
include ViewComponent::Base
|
|
27
|
+
|
|
28
|
+
MSG = "Consider making this method private. " \
|
|
29
|
+
"Only ViewComponent interface methods should be public."
|
|
30
|
+
|
|
31
|
+
ALLOWED_PUBLIC_METHODS = %i[
|
|
32
|
+
initialize
|
|
33
|
+
call
|
|
34
|
+
before_render
|
|
35
|
+
before_render_check
|
|
36
|
+
render?
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
def on_class(node)
|
|
40
|
+
return unless view_component_class?(node)
|
|
41
|
+
|
|
42
|
+
check_public_methods(node)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def check_public_methods(class_node)
|
|
48
|
+
current_visibility = :public
|
|
49
|
+
|
|
50
|
+
class_node.body&.each_child_node do |child|
|
|
51
|
+
if visibility_modifier?(child)
|
|
52
|
+
current_visibility = child.method_name
|
|
53
|
+
next
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
next unless child.def_type?
|
|
57
|
+
next unless current_visibility == :public
|
|
58
|
+
next if ALLOWED_PUBLIC_METHODS.include?(child.method_name)
|
|
59
|
+
|
|
60
|
+
add_offense(child)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def visibility_modifier?(node)
|
|
65
|
+
return false unless node.send_type?
|
|
66
|
+
return false unless node.receiver.nil?
|
|
67
|
+
|
|
68
|
+
%i[private protected public].include?(node.method_name)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module ViewComponent
|
|
6
|
+
# Detects parameters that accept HTML content and suggests using slots.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# class ModalComponent < ViewComponent::Base
|
|
11
|
+
# def initialize(title:, body_html:)
|
|
12
|
+
# @title = title
|
|
13
|
+
# @body_html = body_html
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # good
|
|
18
|
+
# class ModalComponent < ViewComponent::Base
|
|
19
|
+
# renders_one :body
|
|
20
|
+
#
|
|
21
|
+
# def initialize(title:)
|
|
22
|
+
# @title = title
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
class PreferSlots < RuboCop::Cop::Base
|
|
27
|
+
include ViewComponent::Base
|
|
28
|
+
|
|
29
|
+
MSG = "Consider using `%<slot_method>s` instead of passing HTML " \
|
|
30
|
+
"as a parameter. This maintains Rails' automatic HTML escaping."
|
|
31
|
+
|
|
32
|
+
HTML_PARAM_PATTERNS = [
|
|
33
|
+
/_html$/,
|
|
34
|
+
/_content$/,
|
|
35
|
+
/^html_/,
|
|
36
|
+
/^content$/
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
# Exclude common non-HTML parameters
|
|
40
|
+
EXCLUDED_PARAMS = %i[
|
|
41
|
+
html_class
|
|
42
|
+
html_classes
|
|
43
|
+
html_id
|
|
44
|
+
html_tag
|
|
45
|
+
].freeze
|
|
46
|
+
|
|
47
|
+
def_node_search :html_safe_call?, "(send _ :html_safe)"
|
|
48
|
+
|
|
49
|
+
def on_class(node)
|
|
50
|
+
return unless view_component_class?(node)
|
|
51
|
+
|
|
52
|
+
initialize_method = find_initialize(node)
|
|
53
|
+
return unless initialize_method
|
|
54
|
+
|
|
55
|
+
check_initialize_params(initialize_method)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def find_initialize(class_node)
|
|
61
|
+
class_node.each_descendant(:def).find do |def_node|
|
|
62
|
+
def_node.method_name == :initialize
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def check_initialize_params(initialize_node)
|
|
67
|
+
initialize_node.arguments.each do |arg|
|
|
68
|
+
next unless arg.kwoptarg_type? || arg.kwarg_type?
|
|
69
|
+
|
|
70
|
+
param_name = arg.children[0]
|
|
71
|
+
|
|
72
|
+
# Skip excluded parameters
|
|
73
|
+
next if EXCLUDED_PARAMS.include?(param_name)
|
|
74
|
+
|
|
75
|
+
# Check parameter name patterns
|
|
76
|
+
if html_param_name?(param_name)
|
|
77
|
+
suggested_slot = suggest_slot_name(param_name)
|
|
78
|
+
add_offense(arg, message: format(MSG, slot_method: suggested_slot))
|
|
79
|
+
next
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Check for html_safe in default value
|
|
83
|
+
if arg.kwoptarg_type? && html_safe_call?(arg)
|
|
84
|
+
suggested_slot = suggest_slot_name(param_name)
|
|
85
|
+
add_offense(arg, message: format(MSG, slot_method: suggested_slot))
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def html_param_name?(name)
|
|
91
|
+
HTML_PARAM_PATTERNS.any? { |pattern| pattern.match?(name.to_s) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def suggest_slot_name(param_name)
|
|
95
|
+
clean_name = param_name.to_s
|
|
96
|
+
.sub(/_html$/, "")
|
|
97
|
+
.sub(/_content$/, "")
|
|
98
|
+
.sub(/^html_/, "")
|
|
99
|
+
|
|
100
|
+
"renders_one :#{clean_name}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "view_component/base"
|
|
4
|
+
require_relative "view_component/component_suffix"
|
|
5
|
+
require_relative "view_component/no_global_state"
|
|
6
|
+
require_relative "view_component/prefer_private_methods"
|
|
7
|
+
require_relative "view_component/prefer_slots"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lint_roller"
|
|
4
|
+
|
|
5
|
+
module RuboCop
|
|
6
|
+
module ViewComponent
|
|
7
|
+
# A plugin that integrates rubocop-view_component with RuboCop's plugin system.
|
|
8
|
+
class Plugin < LintRoller::Plugin
|
|
9
|
+
def about
|
|
10
|
+
LintRoller::About.new(
|
|
11
|
+
name: "rubocop-view_component",
|
|
12
|
+
version: VERSION,
|
|
13
|
+
homepage: "TODO: Put your plugin's homepage URL here.",
|
|
14
|
+
description: "TODO: Put your plugin's description here."
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def supported?(context)
|
|
19
|
+
context.engine == :rubocop
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def rules(_context)
|
|
23
|
+
LintRoller::Rules.new(
|
|
24
|
+
type: :path,
|
|
25
|
+
config_format: :rubocop,
|
|
26
|
+
value: Pathname.new(__dir__).join("../../../config/default.yml")
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe RuboCop::Cop::ViewComponent::ComponentSuffix, :config do
|
|
4
|
+
let(:config) { RuboCop::Config.new }
|
|
5
|
+
|
|
6
|
+
context "when class inherits from ViewComponent::Base" do
|
|
7
|
+
it "registers an offense when class name does not end with Component" do
|
|
8
|
+
expect_offense(<<~RUBY)
|
|
9
|
+
class FooBar < ViewComponent::Base
|
|
10
|
+
^^^^^^ ViewComponent/ComponentSuffix: ViewComponent class names should end with `Component`.
|
|
11
|
+
end
|
|
12
|
+
RUBY
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "does not register offense when class name ends with Component" do
|
|
16
|
+
expect_no_offenses(<<~RUBY)
|
|
17
|
+
class FooBarComponent < ViewComponent::Base
|
|
18
|
+
end
|
|
19
|
+
RUBY
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
context "when class inherits from ApplicationComponent" do
|
|
24
|
+
it "registers an offense when class name does not end with Component" do
|
|
25
|
+
expect_offense(<<~RUBY)
|
|
26
|
+
class UserCard < ApplicationComponent
|
|
27
|
+
^^^^^^^^ ViewComponent/ComponentSuffix: ViewComponent class names should end with `Component`.
|
|
28
|
+
end
|
|
29
|
+
RUBY
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "does not register offense when class name ends with Component" do
|
|
33
|
+
expect_no_offenses(<<~RUBY)
|
|
34
|
+
class UserCardComponent < ApplicationComponent
|
|
35
|
+
end
|
|
36
|
+
RUBY
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
context "when class does not inherit from ViewComponent" do
|
|
41
|
+
it "does not register offense for regular classes" do
|
|
42
|
+
expect_no_offenses(<<~RUBY)
|
|
43
|
+
class FooBar < SomeOtherBase
|
|
44
|
+
end
|
|
45
|
+
RUBY
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "does not register offense for plain classes" do
|
|
49
|
+
expect_no_offenses(<<~RUBY)
|
|
50
|
+
class FooBar
|
|
51
|
+
end
|
|
52
|
+
RUBY
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
context "with namespaced components" do
|
|
57
|
+
it "checks the final component name" do
|
|
58
|
+
expect_offense(<<~RUBY)
|
|
59
|
+
module Admin
|
|
60
|
+
class UserCard < ViewComponent::Base
|
|
61
|
+
^^^^^^^^ ViewComponent/ComponentSuffix: ViewComponent class names should end with `Component`.
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
RUBY
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "allows namespaced component with Component suffix" do
|
|
68
|
+
expect_no_offenses(<<~RUBY)
|
|
69
|
+
module Admin
|
|
70
|
+
class UserCardComponent < ViewComponent::Base
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
RUBY
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
context "with compact nested class syntax" do
|
|
78
|
+
it "registers offense for compact syntax" do
|
|
79
|
+
expect_offense(<<~RUBY)
|
|
80
|
+
class Admin::UserCard < ViewComponent::Base
|
|
81
|
+
^^^^^^^^^^^^^^^ ViewComponent/ComponentSuffix: ViewComponent class names should end with `Component`.
|
|
82
|
+
end
|
|
83
|
+
RUBY
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe RuboCop::Cop::ViewComponent::NoGlobalState, :config do
|
|
4
|
+
let(:config) { RuboCop::Config.new }
|
|
5
|
+
|
|
6
|
+
context "when accessing params" do
|
|
7
|
+
it "registers an offense" do
|
|
8
|
+
expect_offense(<<~RUBY)
|
|
9
|
+
class UserComponent < ViewComponent::Base
|
|
10
|
+
def admin?
|
|
11
|
+
params[:admin]
|
|
12
|
+
^^^^^^ ViewComponent/NoGlobalState: Avoid accessing `params` directly in ViewComponents. Pass necessary data through the constructor instead.
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
RUBY
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "registers offense for params method call" do
|
|
19
|
+
expect_offense(<<~RUBY)
|
|
20
|
+
class UserComponent < ViewComponent::Base
|
|
21
|
+
def admin?
|
|
22
|
+
params.fetch(:admin)
|
|
23
|
+
^^^^^^ ViewComponent/NoGlobalState: Avoid accessing `params` directly in ViewComponents. Pass necessary data through the constructor instead.
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
RUBY
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context "when accessing request" do
|
|
31
|
+
it "registers an offense" do
|
|
32
|
+
expect_offense(<<~RUBY)
|
|
33
|
+
class UserComponent < ViewComponent::Base
|
|
34
|
+
def user_agent
|
|
35
|
+
request.user_agent
|
|
36
|
+
^^^^^^^ ViewComponent/NoGlobalState: Avoid accessing `request` directly in ViewComponents. Pass necessary data through the constructor instead.
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
RUBY
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
context "when accessing session" do
|
|
44
|
+
it "registers an offense" do
|
|
45
|
+
expect_offense(<<~RUBY)
|
|
46
|
+
class UserComponent < ViewComponent::Base
|
|
47
|
+
def current_user_id
|
|
48
|
+
session[:user_id]
|
|
49
|
+
^^^^^^^ ViewComponent/NoGlobalState: Avoid accessing `session` directly in ViewComponents. Pass necessary data through the constructor instead.
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
RUBY
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
context "when accessing cookies" do
|
|
57
|
+
it "registers an offense" do
|
|
58
|
+
expect_offense(<<~RUBY)
|
|
59
|
+
class UserComponent < ViewComponent::Base
|
|
60
|
+
def preference
|
|
61
|
+
cookies[:theme]
|
|
62
|
+
^^^^^^^ ViewComponent/NoGlobalState: Avoid accessing `cookies` directly in ViewComponents. Pass necessary data through the constructor instead.
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
RUBY
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
context "when accessing flash" do
|
|
70
|
+
it "registers an offense" do
|
|
71
|
+
expect_offense(<<~RUBY)
|
|
72
|
+
class UserComponent < ViewComponent::Base
|
|
73
|
+
def notice
|
|
74
|
+
flash[:notice]
|
|
75
|
+
^^^^^ ViewComponent/NoGlobalState: Avoid accessing `flash` directly in ViewComponents. Pass necessary data through the constructor instead.
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
RUBY
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
context "when not accessing global state" do
|
|
83
|
+
it "does not register offense for instance variables" do
|
|
84
|
+
expect_no_offenses(<<~RUBY)
|
|
85
|
+
class UserComponent < ViewComponent::Base
|
|
86
|
+
def initialize(admin:)
|
|
87
|
+
@admin = admin
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def admin?
|
|
91
|
+
@admin
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
RUBY
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "does not register offense for method arguments" do
|
|
98
|
+
expect_no_offenses(<<~RUBY)
|
|
99
|
+
class UserComponent < ViewComponent::Base
|
|
100
|
+
def format_params(params)
|
|
101
|
+
params[:admin]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
RUBY
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
context "when not in a ViewComponent" do
|
|
109
|
+
it "does not register offense in regular classes" do
|
|
110
|
+
expect_no_offenses(<<~RUBY)
|
|
111
|
+
class RegularClass
|
|
112
|
+
def admin?
|
|
113
|
+
params[:admin]
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
RUBY
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|