rubocop-view_component 0.5.1 → 0.7.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 +4 -4
- data/.rubocop.yml +33 -0
- data/Appraisals +5 -0
- data/CLAUDE.md +6 -14
- data/README.md +18 -7
- data/Rakefile +3 -5
- data/config/default.yml +4 -1
- data/gemfiles/rubocop_minimum.gemfile +13 -0
- data/gemfiles/rubocop_minimum.gemfile.lock +162 -0
- data/lib/rubocop/cop/view_component/base.rb +51 -9
- data/lib/rubocop/cop/view_component/missing_preview.rb +9 -7
- data/lib/rubocop/cop/view_component/no_global_state.rb +14 -0
- data/lib/rubocop/cop/view_component/prefer_private_methods.rb +15 -13
- data/lib/rubocop/cop/view_component/prefer_slots.rb +10 -15
- data/lib/rubocop/cop/view_component/template_analyzer.rb +19 -19
- data/lib/rubocop/cop/view_component/test_rendered_output.rb +4 -1
- data/lib/rubocop/view_component/version.rb +1 -1
- data/script/verify +16 -17
- data/spec/expected_govuk_failures.yml +2 -0
- data/spec/expected_polaris_failures.yml +0 -1
- data/spec/expected_primer_failures.yml +3 -0
- data/spec/rubocop/cop/view_component/base_spec.rb +92 -0
- data/spec/rubocop/cop/view_component/component_suffix_spec.rb +0 -33
- data/spec/rubocop/cop/view_component/missing_preview_spec.rb +83 -5
- data/spec/rubocop/cop/view_component/no_global_state_spec.rb +13 -0
- data/spec/rubocop/cop/view_component/prefer_composition_spec.rb +0 -17
- data/spec/rubocop/cop/view_component/prefer_private_methods_spec.rb +3 -3
- data/spec/rubocop/cop/view_component/test_rendered_output_spec.rb +1 -1
- data/spec/spec_helper.rb +10 -0
- data/spec/support/project_index_helpers.rb +71 -0
- data/verification/govuk_rubocop_config.yml +3 -3
- data/verification/polaris_rubocop_config.yml +3 -4
- data/verification/primer_rubocop_config.yml +5 -4
- metadata +39 -5
|
@@ -16,26 +16,26 @@ module RuboCop
|
|
|
16
16
|
base_path = component_path.sub(/\.rb$/, "")
|
|
17
17
|
component_dir = File.dirname(component_path)
|
|
18
18
|
component_name = File.basename(component_path, ".rb")
|
|
19
|
+
sidecar_dir = File.join(component_dir, component_name)
|
|
19
20
|
|
|
20
|
-
paths =
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
sibling_template = "#{base_path}.html.erb"
|
|
24
|
-
paths << sibling_template if File.exist?(sibling_template)
|
|
25
|
-
|
|
26
|
-
# Check for sidecar template: same_name/same_name.html.erb
|
|
27
|
-
sidecar_template = File.join(component_dir, component_name, "#{component_name}.html.erb")
|
|
28
|
-
paths << sidecar_template if File.exist?(sidecar_template)
|
|
29
|
-
|
|
30
|
-
# Check for variants: same_name.*.html.erb
|
|
31
|
-
variant_pattern = "#{base_path}.*.html.erb"
|
|
32
|
-
paths.concat(Dir.glob(variant_pattern))
|
|
21
|
+
paths = sibling_templates(base_path) + sidecar_templates(sidecar_dir, component_name)
|
|
22
|
+
paths.uniq
|
|
23
|
+
end
|
|
33
24
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
def sibling_templates(base_path)
|
|
26
|
+
paths = []
|
|
27
|
+
sibling = "#{base_path}.html.erb"
|
|
28
|
+
paths << sibling if File.exist?(sibling)
|
|
29
|
+
paths.concat(Dir.glob("#{base_path}.*.html.erb"))
|
|
30
|
+
paths
|
|
31
|
+
end
|
|
37
32
|
|
|
38
|
-
|
|
33
|
+
def sidecar_templates(sidecar_dir, component_name)
|
|
34
|
+
paths = []
|
|
35
|
+
sidecar = File.join(sidecar_dir, "#{component_name}.html.erb")
|
|
36
|
+
paths << sidecar if File.exist?(sidecar)
|
|
37
|
+
paths.concat(Dir.glob(File.join(sidecar_dir, "#{component_name}.*.html.erb")))
|
|
38
|
+
paths
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
# Extract method calls from an ERB template
|
|
@@ -49,7 +49,7 @@ module RuboCop
|
|
|
49
49
|
|
|
50
50
|
# Parse the extracted Ruby code
|
|
51
51
|
parse_ruby_for_method_calls(ruby_code)
|
|
52
|
-
rescue => e
|
|
52
|
+
rescue StandardError => e
|
|
53
53
|
# Graceful degradation on parse errors
|
|
54
54
|
warn "Warning: Failed to parse template #{template_path}: #{e.message}" if ENV["RUBOCOP_DEBUG"]
|
|
55
55
|
Set.new
|
|
@@ -78,7 +78,7 @@ module RuboCop
|
|
|
78
78
|
return unless node.respond_to?(:type)
|
|
79
79
|
|
|
80
80
|
# Look for send nodes with nil receiver (local method calls)
|
|
81
|
-
if node.
|
|
81
|
+
if node.send_type? && node.receiver.nil?
|
|
82
82
|
method_name = node.method_name
|
|
83
83
|
method_calls.add(method_name)
|
|
84
84
|
end
|
|
@@ -28,6 +28,7 @@ module RuboCop
|
|
|
28
28
|
# Check Minitest-style test methods
|
|
29
29
|
def on_def(node)
|
|
30
30
|
return unless within_test_paths?
|
|
31
|
+
|
|
31
32
|
method_name = node.method_name.to_s
|
|
32
33
|
return unless method_name.start_with?("test_")
|
|
33
34
|
return unless instantiates_component?(node)
|
|
@@ -45,6 +46,8 @@ module RuboCop
|
|
|
45
46
|
|
|
46
47
|
add_offense(node)
|
|
47
48
|
end
|
|
49
|
+
alias on_numblock on_block
|
|
50
|
+
alias on_itblock on_block
|
|
48
51
|
|
|
49
52
|
private
|
|
50
53
|
|
|
@@ -58,7 +61,7 @@ module RuboCop
|
|
|
58
61
|
|
|
59
62
|
def instantiates_component?(node)
|
|
60
63
|
node.each_descendant(:send).any? do |send_node|
|
|
61
|
-
next unless send_node.
|
|
64
|
+
next unless send_node.method?(:new)
|
|
62
65
|
|
|
63
66
|
send_node.receiver&.const_type?
|
|
64
67
|
end
|
data/script/verify
CHANGED
|
@@ -68,8 +68,8 @@ def parse_library_arg(libraries)
|
|
|
68
68
|
library_arg = ARGV.find { |arg| !arg.start_with?("--") }
|
|
69
69
|
|
|
70
70
|
if library_arg.nil?
|
|
71
|
-
abort "Usage: #{$PROGRAM_NAME} <library> [--regenerate] [--update]\n" \
|
|
72
|
-
"
|
|
71
|
+
abort "Usage: #{$PROGRAM_NAME} <library> [--regenerate] [--update]\n " \
|
|
72
|
+
"library: #{libraries.keys.join(", ")}"
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
unless libraries.key?(library_arg)
|
|
@@ -79,8 +79,8 @@ def parse_library_arg(libraries)
|
|
|
79
79
|
library_arg
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
def system!(*
|
|
83
|
-
system(
|
|
82
|
+
def system!(*)
|
|
83
|
+
system(*, exception: true)
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
def download_source(dir, tarball_url, display_name)
|
|
@@ -97,10 +97,10 @@ def configure_rubocop(config_file, display_name)
|
|
|
97
97
|
# Merge the library config into the existing config
|
|
98
98
|
library_config.each do |key, value|
|
|
99
99
|
config[key] = if config[key].is_a?(Hash) && value.is_a?(Hash)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
config[key].merge(value)
|
|
101
|
+
else
|
|
102
|
+
value
|
|
103
|
+
end
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
File.write(".rubocop.yml", YAML.dump(config))
|
|
@@ -112,22 +112,23 @@ def add_gem_to_gemfile
|
|
|
112
112
|
end
|
|
113
113
|
|
|
114
114
|
def run_rubocop
|
|
115
|
+
FileUtils.rm_f("Gemfile.lock")
|
|
115
116
|
puts "Running bundle install..."
|
|
116
|
-
system!("bundle", "install")
|
|
117
|
+
system!({ "BUNDLE_WITHOUT" => "development" }, "bundle", "install")
|
|
118
|
+
system!("bundle", "add", "rubocop")
|
|
117
119
|
|
|
118
120
|
puts "Running RuboCop (ViewComponent cops only)..."
|
|
119
121
|
output, status = Open3.capture2(
|
|
120
122
|
"bundle", "exec", "rubocop",
|
|
121
|
-
"--
|
|
123
|
+
"--plugin", "rubocop-view_component",
|
|
122
124
|
"--only", "ViewComponent",
|
|
125
|
+
"--config", ".rubocop.yml",
|
|
123
126
|
"--format", "json"
|
|
124
127
|
)
|
|
125
128
|
|
|
126
129
|
puts "RuboCop exit status: #{status.exitstatus}"
|
|
127
130
|
|
|
128
|
-
if output.strip.empty?
|
|
129
|
-
abort "ERROR: RuboCop produced no output (exit status: #{status.exitstatus})"
|
|
130
|
-
end
|
|
131
|
+
abort "ERROR: RuboCop produced no output (exit status: #{status.exitstatus})" if output.strip.empty?
|
|
131
132
|
|
|
132
133
|
output
|
|
133
134
|
end
|
|
@@ -166,11 +167,9 @@ def load_expected(results_file)
|
|
|
166
167
|
end
|
|
167
168
|
|
|
168
169
|
def verify(offenses, results_file)
|
|
169
|
-
unless File.exist?(results_file)
|
|
170
|
-
abort "ERROR: #{results_file} not found. Run '#{$PROGRAM_NAME} --regenerate' first."
|
|
171
|
-
end
|
|
170
|
+
abort "ERROR: #{results_file} not found. Run '#{$PROGRAM_NAME} --regenerate' first." unless File.exist?(results_file)
|
|
172
171
|
|
|
173
|
-
expected = load_expected(results_file)
|
|
172
|
+
expected = load_expected(results_file) || {}
|
|
174
173
|
|
|
175
174
|
if offenses == expected
|
|
176
175
|
puts "Verification passed: output matches #{results_file}"
|
|
@@ -7,10 +7,12 @@ ViewComponent/ComponentSuffix:
|
|
|
7
7
|
- 'app/components/govuk_component/notification_banner_component.rb'
|
|
8
8
|
- 'app/components/govuk_component/pagination_component/adjacent_page.rb'
|
|
9
9
|
- 'app/components/govuk_component/pagination_component/item.rb'
|
|
10
|
+
- 'app/components/govuk_component/panel_component.rb'
|
|
10
11
|
- 'app/components/govuk_component/tab_component.rb'
|
|
11
12
|
ViewComponent/PreferPrivateMethods:
|
|
12
13
|
- 'app/components/govuk_component/base.rb'
|
|
13
14
|
- 'app/components/govuk_component/notification_banner_component.rb'
|
|
15
|
+
- 'app/components/govuk_component/panel_component.rb'
|
|
14
16
|
- 'app/components/govuk_component/service_navigation_component.rb'
|
|
15
17
|
- 'app/components/govuk_component/tab_component.rb'
|
|
16
18
|
ViewComponent/TestRenderedOutput:
|
|
@@ -22,7 +22,6 @@ ViewComponent/MissingPreview:
|
|
|
22
22
|
- 'app/components/polaris/placeholder_component.rb'
|
|
23
23
|
- 'app/components/polaris/shopify_navigation_component.rb'
|
|
24
24
|
- 'app/components/polaris/text_field_component.rb'
|
|
25
|
-
- 'app/components/polaris/toast_component.rb'
|
|
26
25
|
ViewComponent/NoGlobalState:
|
|
27
26
|
- 'app/components/polaris/shopify_navigation_component.rb'
|
|
28
27
|
ViewComponent/PreferPrivateMethods:
|
|
@@ -15,5 +15,8 @@ ViewComponent/PreferPrivateMethods:
|
|
|
15
15
|
- 'app/components/primer/alpha/tree_view/node.rb'
|
|
16
16
|
- 'app/components/primer/beta/avatar.rb'
|
|
17
17
|
- 'app/components/primer/beta/nav_list.rb'
|
|
18
|
+
- 'app/components/primer/beta/nav_list/divider.rb'
|
|
19
|
+
- 'app/components/primer/beta/nav_list/group.rb'
|
|
20
|
+
- 'app/components/primer/beta/nav_list/item.rb'
|
|
18
21
|
ViewComponent/TestRenderedOutput:
|
|
19
22
|
- 'test/components/alpha/action_list_test.rb'
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe RuboCop::Cop::ViewComponent::Base do
|
|
4
|
+
let(:cop_class) { RuboCop::Cop::ViewComponent::ComponentSuffix }
|
|
5
|
+
let(:cop) { cop_class.new(RuboCop::Config.new) }
|
|
6
|
+
|
|
7
|
+
before do
|
|
8
|
+
cop.project_index = nil
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
describe "#view_component_class?" do
|
|
12
|
+
it "raises when project_index is not set and class is not in ComponentNamespaces" do
|
|
13
|
+
source = parse_source(<<~RUBY)
|
|
14
|
+
class Foo < ViewComponent::Base
|
|
15
|
+
end
|
|
16
|
+
RUBY
|
|
17
|
+
|
|
18
|
+
expect { cop.send(:view_component_class?, source.ast) }.to raise_error(
|
|
19
|
+
RuntimeError,
|
|
20
|
+
/UseProjectIndex/
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "does not raise when class matches a ComponentNamespace" do
|
|
25
|
+
config = RuboCop::Config.new(
|
|
26
|
+
"ViewComponent/ComponentSuffix" => {
|
|
27
|
+
"ComponentNamespaces" => ["MyApp::"]
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
cop = cop_class.new(config)
|
|
31
|
+
cop.project_index = nil
|
|
32
|
+
|
|
33
|
+
source = parse_source(<<~RUBY)
|
|
34
|
+
module MyApp
|
|
35
|
+
class Foo < ViewComponent::Base
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
RUBY
|
|
39
|
+
|
|
40
|
+
class_node = source.ast.body
|
|
41
|
+
|
|
42
|
+
expect(cop.send(:view_component_class?, class_node)).to be true
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe "#view_component_parent?" do
|
|
47
|
+
it "raises when project_index is not set" do
|
|
48
|
+
source = parse_source(<<~RUBY)
|
|
49
|
+
class Foo < ViewComponent::Base
|
|
50
|
+
end
|
|
51
|
+
RUBY
|
|
52
|
+
|
|
53
|
+
parent = source.ast.parent_class
|
|
54
|
+
|
|
55
|
+
expect { cop.send(:view_component_parent?, parent) }.to raise_error(
|
|
56
|
+
RuntimeError,
|
|
57
|
+
/UseProjectIndex/
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe "#view_component_parent_class?" do
|
|
63
|
+
it "raises when project_index is not set" do
|
|
64
|
+
source = parse_source(<<~RUBY)
|
|
65
|
+
class Foo < ViewComponent::Base
|
|
66
|
+
end
|
|
67
|
+
RUBY
|
|
68
|
+
|
|
69
|
+
expect { cop.send(:view_component_parent_class?, source.ast) }.to raise_error(
|
|
70
|
+
RuntimeError,
|
|
71
|
+
/UseProjectIndex/
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
describe "#inside_view_component?" do
|
|
77
|
+
it "raises when project_index is not set" do
|
|
78
|
+
source = parse_source(<<~RUBY)
|
|
79
|
+
class Foo < ViewComponent::Base
|
|
80
|
+
def call; end
|
|
81
|
+
end
|
|
82
|
+
RUBY
|
|
83
|
+
|
|
84
|
+
method_node = source.ast.body
|
|
85
|
+
|
|
86
|
+
expect { cop.send(:inside_view_component?, method_node) }.to raise_error(
|
|
87
|
+
RuntimeError,
|
|
88
|
+
/UseProjectIndex/
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -74,39 +74,6 @@ RSpec.describe RuboCop::Cop::ViewComponent::ComponentSuffix, :config do
|
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
context "when ViewComponentParentClasses is configured" do
|
|
78
|
-
let(:config) do
|
|
79
|
-
RuboCop::Config.new(
|
|
80
|
-
"ViewComponent/ComponentSuffix" => {
|
|
81
|
-
"ViewComponentParentClasses" => ["Primer::Component"]
|
|
82
|
-
}
|
|
83
|
-
)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
it "registers an offense for classes inheriting from a configured parent" do
|
|
87
|
-
expect_offense(<<~RUBY)
|
|
88
|
-
class FooBar < Primer::Component
|
|
89
|
-
^^^^^^ ViewComponent class names should end with `Component`.
|
|
90
|
-
end
|
|
91
|
-
RUBY
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
it "does not register offense when class name ends with Component" do
|
|
95
|
-
expect_no_offenses(<<~RUBY)
|
|
96
|
-
class FooBarComponent < Primer::Component
|
|
97
|
-
end
|
|
98
|
-
RUBY
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
it "still recognizes the default parent classes" do
|
|
102
|
-
expect_offense(<<~RUBY)
|
|
103
|
-
class FooBar < ViewComponent::Base
|
|
104
|
-
^^^^^^ ViewComponent class names should end with `Component`.
|
|
105
|
-
end
|
|
106
|
-
RUBY
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
77
|
context "with compact nested class syntax" do
|
|
111
78
|
it "registers offense for compact syntax" do
|
|
112
79
|
expect_offense(<<~RUBY)
|
|
@@ -12,7 +12,8 @@ RSpec.describe RuboCop::Cop::ViewComponent::MissingPreview, :config do
|
|
|
12
12
|
|
|
13
13
|
context "when a preview file exists" do
|
|
14
14
|
it "does not register an offense" do
|
|
15
|
-
allow(File).to receive(:exist?).and_return(
|
|
15
|
+
allow(File).to receive(:exist?).and_return(false)
|
|
16
|
+
allow(File).to receive(:exist?).with("/previews/user_preview.rb").and_return(true)
|
|
16
17
|
|
|
17
18
|
expect_no_offenses(<<~RUBY, "/app/components/user_component.rb")
|
|
18
19
|
class UserComponent < ViewComponent::Base
|
|
@@ -23,7 +24,8 @@ RSpec.describe RuboCop::Cop::ViewComponent::MissingPreview, :config do
|
|
|
23
24
|
|
|
24
25
|
context "when no preview file exists" do
|
|
25
26
|
it "registers an offense" do
|
|
26
|
-
allow(File).to receive(:exist?).and_return(false)
|
|
27
|
+
allow(File).to receive(:exist?).with("/previews/user_preview.rb").and_return(false)
|
|
28
|
+
allow(File).to receive(:exist?).with("/previews/user_component_preview.rb").and_return(false)
|
|
27
29
|
|
|
28
30
|
expect_offense(<<~RUBY, "/app/components/user_component.rb")
|
|
29
31
|
class UserComponent < ViewComponent::Base
|
|
@@ -45,7 +47,10 @@ RSpec.describe RuboCop::Cop::ViewComponent::MissingPreview, :config do
|
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
it "registers an offense for a component in a configured namespace" do
|
|
48
|
-
allow(File).to receive(:exist?).and_return(false)
|
|
50
|
+
allow(File).to receive(:exist?).with("/previews/v2/table_preview.rb").and_return(false)
|
|
51
|
+
allow(File).to receive(:exist?).with("/previews/v2/table_component_preview.rb").and_return(false)
|
|
52
|
+
allow(File).to receive(:exist?).with("/previews/table_preview.rb").and_return(false)
|
|
53
|
+
allow(File).to receive(:exist?).with("/previews/table_component_preview.rb").and_return(false)
|
|
49
54
|
|
|
50
55
|
expect_offense(<<~RUBY, "/app/components/v2/table.rb")
|
|
51
56
|
class V2::Table < SomeBase
|
|
@@ -82,16 +87,89 @@ RSpec.describe RuboCop::Cop::ViewComponent::MissingPreview, :config do
|
|
|
82
87
|
end
|
|
83
88
|
RUBY
|
|
84
89
|
end
|
|
90
|
+
|
|
91
|
+
it "registers an offense for a component declared with nested module form when no preview exists" do
|
|
92
|
+
allow(File).to receive(:exist?).and_return(false)
|
|
93
|
+
|
|
94
|
+
expect_offense(<<~RUBY, "/app/components/v2/table.rb")
|
|
95
|
+
module V2
|
|
96
|
+
class Table < SomeBase
|
|
97
|
+
^^^^^ No preview found for V2::Table (looked in: /previews).
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
RUBY
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
it "does not register an offense for a component declared with nested module form when preview exists" do
|
|
104
|
+
allow(File).to receive(:exist?).and_return(false)
|
|
105
|
+
allow(File).to receive(:exist?).with("/previews/v2/grouped_multi_select_preview.rb").and_return(true)
|
|
106
|
+
|
|
107
|
+
expect_no_offenses(<<~RUBY, "/app/components/v2/grouped_multi_select.rb")
|
|
108
|
+
module V2
|
|
109
|
+
class GroupedMultiSelect < V2::MultiSelect
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
RUBY
|
|
113
|
+
end
|
|
85
114
|
end
|
|
86
115
|
|
|
87
|
-
context "when
|
|
88
|
-
it "does not register an offense" do
|
|
116
|
+
context "when the component is declared with nested module form" do
|
|
117
|
+
it "does not register an offense when preview exists at the namespaced path" do
|
|
118
|
+
allow(File).to receive(:exist?).and_return(false)
|
|
119
|
+
allow(File).to receive(:exist?).with("/previews/admin/page_component_preview.rb").and_return(true)
|
|
120
|
+
|
|
121
|
+
expect_no_offenses(<<~RUBY, "/app/components/admin/page_component.rb")
|
|
122
|
+
module Admin
|
|
123
|
+
class PageComponent < ApplicationComponent
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
RUBY
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "registers an offense when no preview exists" do
|
|
130
|
+
allow(File).to receive(:exist?).and_return(false)
|
|
131
|
+
|
|
132
|
+
expect_offense(<<~RUBY, "/app/components/admin/page_component.rb")
|
|
133
|
+
module Admin
|
|
134
|
+
class PageComponent < ApplicationComponent
|
|
135
|
+
^^^^^^^^^^^^^ No preview found for Admin::PageComponent (looked in: /previews).
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
RUBY
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
context "when the namespace contains an acronym" do
|
|
143
|
+
it "does not register an offense when preview exists at the correct path" do
|
|
144
|
+
ActiveSupport::Inflector.inflections(:en) { |i| i.acronym "UI" }
|
|
145
|
+
|
|
89
146
|
allow(File).to receive(:exist?).and_return(false)
|
|
147
|
+
allow(File).to receive(:exist?).with("/previews/ui/button_component_preview.rb").and_return(true)
|
|
90
148
|
|
|
149
|
+
expect_no_offenses(<<~RUBY, "/app/components/ui/button_component.rb")
|
|
150
|
+
module UI
|
|
151
|
+
class ButtonComponent < ViewComponent::Base
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
RUBY
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
context "when not a ViewComponent" do
|
|
159
|
+
it "does not register an offense" do
|
|
91
160
|
expect_no_offenses(<<~RUBY, "/app/components/user_component.rb")
|
|
92
161
|
class UserComponent
|
|
93
162
|
end
|
|
94
163
|
RUBY
|
|
95
164
|
end
|
|
96
165
|
end
|
|
166
|
+
|
|
167
|
+
context "when the class is itself a registered parent class" do
|
|
168
|
+
it "does not register an offense for a built-in parent class" do
|
|
169
|
+
expect_no_offenses(<<~RUBY, "/app/components/application_component.rb")
|
|
170
|
+
class ApplicationComponent < ViewComponent::Base
|
|
171
|
+
end
|
|
172
|
+
RUBY
|
|
173
|
+
end
|
|
174
|
+
end
|
|
97
175
|
end
|
|
@@ -105,6 +105,19 @@ RSpec.describe RuboCop::Cop::ViewComponent::NoGlobalState, :config do
|
|
|
105
105
|
end
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
+
context "with Sorbet signatures" do
|
|
109
|
+
it "does not register offense for params in sig block" do
|
|
110
|
+
expect_no_offenses(<<~RUBY)
|
|
111
|
+
class UserComponent < ViewComponent::Base
|
|
112
|
+
sig { params(descriptor: String, admin_view: T::Boolean).returns(T.nilable(String)) }
|
|
113
|
+
def call(descriptor:, admin_view:)
|
|
114
|
+
@descriptor = descriptor
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
RUBY
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
108
121
|
context "when not in a ViewComponent" do
|
|
109
122
|
it "does not register offense in regular classes" do
|
|
110
123
|
expect_no_offenses(<<~RUBY)
|
|
@@ -88,21 +88,4 @@ RSpec.describe RuboCop::Cop::ViewComponent::PreferComposition, :config do
|
|
|
88
88
|
RUBY
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
|
-
|
|
92
|
-
context "when ViewComponentParentClasses is configured" do
|
|
93
|
-
let(:config) do
|
|
94
|
-
RuboCop::Config.new(
|
|
95
|
-
"ViewComponent/PreferComposition" => {
|
|
96
|
-
"ViewComponentParentClasses" => ["Primer::Component"]
|
|
97
|
-
}
|
|
98
|
-
)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
it "does not register an offense for configured parent classes" do
|
|
102
|
-
expect_no_offenses(<<~RUBY)
|
|
103
|
-
class FooBarComponent < Primer::Component
|
|
104
|
-
end
|
|
105
|
-
RUBY
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
91
|
end
|
|
@@ -144,7 +144,7 @@ RSpec.describe RuboCop::Cop::ViewComponent::PreferPrivateMethods, :config do
|
|
|
144
144
|
context "with custom patterns" do
|
|
145
145
|
let(:config) do
|
|
146
146
|
RuboCop::Config.new(
|
|
147
|
-
"AllCops" => {"DisplayCopNames" => true},
|
|
147
|
+
"AllCops" => { "DisplayCopNames" => true },
|
|
148
148
|
"ViewComponent/PreferPrivateMethods" => {
|
|
149
149
|
"AllowedPublicMethods" => %w[initialize call],
|
|
150
150
|
"AllowedPublicMethodPatterns" => ["^render_", "^with_"]
|
|
@@ -186,7 +186,7 @@ RSpec.describe RuboCop::Cop::ViewComponent::PreferPrivateMethods, :config do
|
|
|
186
186
|
context "with custom AllowedPublicMethods" do
|
|
187
187
|
let(:config) do
|
|
188
188
|
RuboCop::Config.new(
|
|
189
|
-
"AllCops" => {"DisplayCopNames" => true},
|
|
189
|
+
"AllCops" => { "DisplayCopNames" => true },
|
|
190
190
|
"ViewComponent/PreferPrivateMethods" => {
|
|
191
191
|
"AllowedPublicMethods" => %w[initialize call custom_public_method],
|
|
192
192
|
"AllowedPublicMethodPatterns" => []
|
|
@@ -261,7 +261,7 @@ RSpec.describe RuboCop::Cop::ViewComponent::PreferPrivateMethods, :config do
|
|
|
261
261
|
context "with template files" do
|
|
262
262
|
let(:component_file) { "spec/fixtures/components/template_method_component.rb" }
|
|
263
263
|
|
|
264
|
-
it "
|
|
264
|
+
it "registers methods called in template, but flags unused methods" do
|
|
265
265
|
expect_offense(<<~RUBY, component_file)
|
|
266
266
|
# frozen_string_literal: true
|
|
267
267
|
|
|
@@ -99,7 +99,7 @@ RSpec.describe RuboCop::Cop::ViewComponent::TestRenderedOutput, :config do
|
|
|
99
99
|
context "with TestPaths configured" do
|
|
100
100
|
let(:config) do
|
|
101
101
|
RuboCop::Config.new(
|
|
102
|
-
"AllCops" => {"DisplayCopNames" => true},
|
|
102
|
+
"AllCops" => { "DisplayCopNames" => true },
|
|
103
103
|
"ViewComponent/TestRenderedOutput" => {
|
|
104
104
|
"TestPaths" => ["spec/components/v2/"]
|
|
105
105
|
}
|
data/spec/spec_helper.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "rubocop-view_component"
|
|
4
4
|
require "rubocop/rspec/support"
|
|
5
|
+
require_relative "support/project_index_helpers"
|
|
5
6
|
|
|
6
7
|
RSpec.configure do |config|
|
|
7
8
|
config.disable_monkey_patching!
|
|
@@ -11,4 +12,13 @@ RSpec.configure do |config|
|
|
|
11
12
|
|
|
12
13
|
config.order = :random
|
|
13
14
|
Kernel.srand config.seed
|
|
15
|
+
|
|
16
|
+
config.include ViewComponent::ProjectIndexHelpers
|
|
17
|
+
|
|
18
|
+
config.before do |example|
|
|
19
|
+
next unless example.metadata[:config]
|
|
20
|
+
|
|
21
|
+
graph = build_index
|
|
22
|
+
cop.project_index = ViewComponent::ProjectIndexHelpers::LazyIndex.new(graph, cop)
|
|
23
|
+
end
|
|
14
24
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubydex"
|
|
4
|
+
|
|
5
|
+
module ViewComponent
|
|
6
|
+
module ProjectIndexHelpers
|
|
7
|
+
DEFAULT_SOURCES = {
|
|
8
|
+
"file:///view_component.rb" => <<~RUBY,
|
|
9
|
+
module ViewComponent
|
|
10
|
+
class Base
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
RUBY
|
|
14
|
+
"file:///application_component.rb" => <<~RUBY,
|
|
15
|
+
class ApplicationComponent < ViewComponent::Base
|
|
16
|
+
end
|
|
17
|
+
RUBY
|
|
18
|
+
"file:///user_component.rb" => <<~RUBY
|
|
19
|
+
class UserComponent < ApplicationComponent
|
|
20
|
+
end
|
|
21
|
+
RUBY
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
def build_index(sources = {})
|
|
25
|
+
graph = Rubydex::Graph.new
|
|
26
|
+
DEFAULT_SOURCES.merge(sources).each { |uri, source| graph.index_source(uri, source, "ruby") }
|
|
27
|
+
graph.resolve
|
|
28
|
+
graph
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Wraps a graph to lazily index the source being inspected when
|
|
32
|
+
# resolve_constant is called. This mirrors how the real RuboCop runner
|
|
33
|
+
# indexes all project files, including the one being inspected.
|
|
34
|
+
class LazyIndex
|
|
35
|
+
def initialize(base_graph, cop)
|
|
36
|
+
@base_graph = base_graph
|
|
37
|
+
@cop = cop
|
|
38
|
+
@rebuilt = false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def resolve_constant(name, nesting)
|
|
42
|
+
result = @base_graph.resolve_constant(name, nesting)
|
|
43
|
+
return result if result
|
|
44
|
+
|
|
45
|
+
rebuild_once
|
|
46
|
+
@base_graph.resolve_constant(name, nesting)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def method_missing(name, ...)
|
|
50
|
+
@base_graph.send(name, ...)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def respond_to_missing?(name, include_private = false)
|
|
54
|
+
@base_graph.respond_to?(name, include_private) || super
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def rebuild_once
|
|
60
|
+
return if @rebuilt
|
|
61
|
+
|
|
62
|
+
@rebuilt = true
|
|
63
|
+
source = @cop&.processed_source
|
|
64
|
+
return unless source&.raw_source
|
|
65
|
+
|
|
66
|
+
@base_graph.index_source("file:///test_source.rb", source.raw_source, "ruby")
|
|
67
|
+
@base_graph.resolve
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|