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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +33 -0
  3. data/Appraisals +5 -0
  4. data/CLAUDE.md +6 -14
  5. data/README.md +18 -7
  6. data/Rakefile +3 -5
  7. data/config/default.yml +4 -1
  8. data/gemfiles/rubocop_minimum.gemfile +13 -0
  9. data/gemfiles/rubocop_minimum.gemfile.lock +162 -0
  10. data/lib/rubocop/cop/view_component/base.rb +51 -9
  11. data/lib/rubocop/cop/view_component/missing_preview.rb +9 -7
  12. data/lib/rubocop/cop/view_component/no_global_state.rb +14 -0
  13. data/lib/rubocop/cop/view_component/prefer_private_methods.rb +15 -13
  14. data/lib/rubocop/cop/view_component/prefer_slots.rb +10 -15
  15. data/lib/rubocop/cop/view_component/template_analyzer.rb +19 -19
  16. data/lib/rubocop/cop/view_component/test_rendered_output.rb +4 -1
  17. data/lib/rubocop/view_component/version.rb +1 -1
  18. data/script/verify +16 -17
  19. data/spec/expected_govuk_failures.yml +2 -0
  20. data/spec/expected_polaris_failures.yml +0 -1
  21. data/spec/expected_primer_failures.yml +3 -0
  22. data/spec/rubocop/cop/view_component/base_spec.rb +92 -0
  23. data/spec/rubocop/cop/view_component/component_suffix_spec.rb +0 -33
  24. data/spec/rubocop/cop/view_component/missing_preview_spec.rb +83 -5
  25. data/spec/rubocop/cop/view_component/no_global_state_spec.rb +13 -0
  26. data/spec/rubocop/cop/view_component/prefer_composition_spec.rb +0 -17
  27. data/spec/rubocop/cop/view_component/prefer_private_methods_spec.rb +3 -3
  28. data/spec/rubocop/cop/view_component/test_rendered_output_spec.rb +1 -1
  29. data/spec/spec_helper.rb +10 -0
  30. data/spec/support/project_index_helpers.rb +71 -0
  31. data/verification/govuk_rubocop_config.yml +3 -3
  32. data/verification/polaris_rubocop_config.yml +3 -4
  33. data/verification/primer_rubocop_config.yml +5 -4
  34. 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
- # Check for sibling template: same_name.html.erb
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
- # Check for sidecar variants: same_name/same_name.*.html.erb
35
- sidecar_variant_pattern = File.join(component_dir, component_name, "#{component_name}.*.html.erb")
36
- paths.concat(Dir.glob(sidecar_variant_pattern))
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
- paths.uniq
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.type == :send && node.receiver.nil?
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.method_name == :new
64
+ next unless send_node.method?(:new)
62
65
 
63
66
  send_node.receiver&.const_type?
64
67
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module ViewComponent
5
- VERSION = "0.5.1"
5
+ VERSION = "0.7.0"
6
6
  end
7
7
  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
- " library: #{libraries.keys.join(", ")}"
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!(*args)
83
- system(*args, exception: true)
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
- config[key].merge(value)
101
- else
102
- value
103
- end
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
- "--require", "rubocop-view_component",
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(true)
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 not a ViewComponent" do
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 "does not flag methods called in template, but flags unused methods" do
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
@@ -1,6 +1,6 @@
1
- ViewComponent:
2
- ViewComponentParentClasses:
3
- - GovukComponent::Base
1
+ AllCops:
2
+ UseProjectIndex: true
3
+ ProjectIndexIncludesGems: true
4
4
 
5
5
  # x-govuk components do not use ViewComponent previews
6
6
  ViewComponent/MissingPreview:
@@ -1,7 +1,6 @@
1
- ViewComponent:
2
- ViewComponentParentClasses:
3
- - Polaris::Component
4
- - Component
1
+ AllCops:
2
+ UseProjectIndex: true
3
+ ProjectIndexIncludesGems: true
5
4
 
6
5
  ViewComponent/MissingPreview:
7
6
  PreviewPaths: