rubocop-view_component 0.7.0 → 0.8.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.
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "rubydex"
5
+ require "uri"
6
+
7
+ module RuboCop
8
+ module Cop
9
+ module ViewComponent
10
+ # Extends the project index with `AdditionalSourcePaths`: source
11
+ # directories the cops index themselves, so a chain that passes through a
12
+ # gem RuboCop does not index is still resolvable. The motivating case is a
13
+ # Rails engine's `app/components`, which `AllCops/ProjectIndexIncludesGems`
14
+ # misses because it only indexes a gem's `lib`.
15
+ module SupplementaryIndex
16
+ # A minimal stand-in for the framework base class, indexed alongside the
17
+ # configured paths so ancestry there can still reach
18
+ # `ViewComponent::Base` without indexing the whole view_component gem.
19
+ BASE_URI = "rubydex:view_component_base"
20
+ BASE_SOURCE = <<~RUBY
21
+ module ViewComponent
22
+ class Base
23
+ end
24
+ end
25
+ RUBY
26
+
27
+ # RuboCop's cache key, extended with the supplementary files' mtimes:
28
+ # those files are not part of RuboCop's own index documents, so a change
29
+ # to one would otherwise not invalidate a cached result.
30
+ def external_dependency_checksum
31
+ base = super
32
+ extra = supplementary_files_checksum
33
+ return base if extra.nil?
34
+ return extra if base.nil?
35
+
36
+ Digest::SHA1.hexdigest("#{base}:#{extra}")
37
+ end
38
+
39
+ private
40
+
41
+ # Resolve through RuboCop's project index first, then the supplementary one.
42
+ def resolve_constant_in_index(const_node)
43
+ super || resolve_constant_in_graph(supplementary_index, const_node)
44
+ end
45
+
46
+ def resolve_constant_in_graph(graph, const_node)
47
+ return nil unless graph
48
+
49
+ segments = const_node.const_name.split("::")
50
+ # The inspected file's lexical scopes live in the project index, not
51
+ # in the supplementary one, so resolving through them can fail (a
52
+ # scope name like `Ui` is simply absent here). Try them first, then
53
+ # resolve from the top level.
54
+ nesting_candidates(const_node).each do |nesting|
55
+ declaration = resolve_segments(graph, segments, nesting)
56
+ return declaration if declaration
57
+ end
58
+ nil
59
+ end
60
+
61
+ def nesting_candidates(const_node)
62
+ return [[]] if const_node.absolute?
63
+
64
+ [lexical_nesting_of(const_node), []].uniq
65
+ end
66
+
67
+ def resolve_segments(graph, segments, nesting)
68
+ declaration = graph.resolve_constant(segments.first, nesting)
69
+ return nil unless declaration
70
+
71
+ segments.drop(1).each do |segment|
72
+ return nil unless declaration.is_a?(Rubydex::Namespace)
73
+
74
+ declaration = declaration.find_member(segment)
75
+ return nil unless declaration
76
+ end
77
+ declaration
78
+ end
79
+
80
+ # Built once per cop and only when the option is set.
81
+ def supplementary_index
82
+ return @supplementary_index if defined?(@supplementary_index)
83
+
84
+ @supplementary_index = build_supplementary_index
85
+ end
86
+
87
+ def build_supplementary_index
88
+ files = supplementary_source_files
89
+ return nil if files.empty?
90
+
91
+ graph = Rubydex::Graph.new
92
+ graph.index_source(BASE_URI, BASE_SOURCE, "ruby")
93
+ index_files(graph, files)
94
+ graph.resolve
95
+ graph
96
+ rescue StandardError
97
+ nil
98
+ end
99
+
100
+ def index_files(graph, files)
101
+ # File by file, not `index_all`: in a forked RuboCop worker
102
+ # `index_all` crashes once the parent has built its own graph, while
103
+ # `index_source` is safe.
104
+ files.each do |file|
105
+ graph.index_source(file_uri(file), File.read(file), "ruby")
106
+ rescue StandardError
107
+ next
108
+ end
109
+ end
110
+
111
+ def file_uri(file)
112
+ "file://#{URI::DEFAULT_PARSER.escape(file)}"
113
+ end
114
+
115
+ def supplementary_source_files
116
+ configured_source_paths.flat_map { |path| Dir.glob(File.join(path, "**", "*.rb")) }.uniq.sort
117
+ end
118
+
119
+ def configured_source_paths
120
+ Array(cop_config["AdditionalSourcePaths"]).filter_map do |path|
121
+ expanded = File.expand_path(path, config.base_dir_for_path_parameters)
122
+ expanded if File.directory?(expanded)
123
+ end
124
+ end
125
+
126
+ def supplementary_files_checksum
127
+ files = supplementary_source_files
128
+ return nil if files.empty?
129
+
130
+ Digest::SHA1.hexdigest(files.map { |file| file_signature(file) }.join("\n"))
131
+ end
132
+
133
+ def file_signature(file)
134
+ stat = File.stat(file)
135
+ "#{file}:#{stat.mtime.to_f}:#{stat.size}"
136
+ rescue StandardError
137
+ file
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "rspec_scope"
4
+
3
5
  module RuboCop
4
6
  module Cop
5
7
  module ViewComponent
@@ -8,6 +10,11 @@ module RuboCop
8
10
  #
9
11
  # This cop is only enabled for test files by default (see config).
10
12
  #
13
+ # A test is considered to instantiate a component when it builds one
14
+ # directly (`UserComponent.new`), through `described_class.new` inside a
15
+ # `describe UserComponent` block, or by referencing a `let`/`subject`
16
+ # helper whose body instantiates a component.
17
+ #
11
18
  # @example
12
19
  # # bad
13
20
  # def test_formatted_title
@@ -21,10 +28,40 @@ module RuboCop
21
28
  # assert_text "HELLO"
22
29
  # end
23
30
  #
31
+ # # bad
32
+ # describe TitleComponent do
33
+ # it "formats the title" do
34
+ # component = described_class.new("hello")
35
+ # expect(component.formatted_title).to eq("HELLO")
36
+ # end
37
+ # end
38
+ #
39
+ # # good
40
+ # describe TitleComponent do
41
+ # it "formats the title" do
42
+ # render_inline described_class.new("hello")
43
+ # expect(page).to have_text("HELLO")
44
+ # end
45
+ # end
46
+ #
47
+ # # bad
48
+ # let(:component) { TitleComponent.new("hello") }
49
+ #
50
+ # it "formats the title" do
51
+ # expect(component.formatted_title).to eq("HELLO")
52
+ # end
53
+ #
24
54
  class TestRenderedOutput < RuboCop::Cop::Base
55
+ include ViewComponent::Base
56
+ include RSpecScope
57
+
25
58
  MSG = "Test instantiates a component but doesn't use `render_inline` or `render_preview`. " \
26
59
  "Test the rendered output instead of component methods directly."
27
60
 
61
+ # `render_in_view_context` renders through a view context (it is what
62
+ # form-builder tests need), so it is a rendered-output test too.
63
+ RENDER_METHODS = %i[render_inline render_preview render_in_view_context].freeze
64
+
28
65
  # Check Minitest-style test methods
29
66
  def on_def(node)
30
67
  return unless within_test_paths?
@@ -32,7 +69,7 @@ module RuboCop
32
69
  method_name = node.method_name.to_s
33
70
  return unless method_name.start_with?("test_")
34
71
  return unless instantiates_component?(node)
35
- return if contains_render_method?(node)
72
+ return if renders_output?(node)
36
73
 
37
74
  add_offense(node)
38
75
  end
@@ -40,9 +77,9 @@ module RuboCop
40
77
  # Check RSpec-style it blocks
41
78
  def on_block(node)
42
79
  return unless within_test_paths?
43
- return unless rspec_it_block?(node)
80
+ return unless rspec_example_block?(node)
44
81
  return unless instantiates_component?(node)
45
- return if contains_render_method?(node)
82
+ return if renders_output?(node)
46
83
 
47
84
  add_offense(node)
48
85
  end
@@ -60,24 +97,82 @@ module RuboCop
60
97
  end
61
98
 
62
99
  def instantiates_component?(node)
63
- node.each_descendant(:send).any? do |send_node|
100
+ component_new_call?(node) ||
101
+ referenced_helper?(node) { |body| component_new_call?(body) }
102
+ end
103
+
104
+ def renders_output?(node)
105
+ render_call?(node) ||
106
+ referenced_helper?(node) { |body| render_call?(body) }
107
+ end
108
+
109
+ # True when the node calls `.new` on a component constant or on
110
+ # `described_class` within a `describe` block.
111
+ def component_new_call?(node)
112
+ node.each_node(:send).any? do |send_node|
64
113
  next unless send_node.method?(:new)
114
+ next if inside_error_expectation?(send_node)
65
115
 
66
- send_node.receiver&.const_type?
116
+ component_receiver?(send_node.receiver)
67
117
  end
68
118
  end
69
119
 
70
- def contains_render_method?(node)
71
- node.each_descendant(:send).any? do |send_node|
72
- %i[render_inline render_preview].include?(send_node.method_name)
120
+ # A component built inside an error assertion is exercising
121
+ # initialisation behaviour rather than rendered output, so there is
122
+ # nothing to render. Covers RSpec's `expect { ... }.to raise_error` and
123
+ # Minitest's `assert_raises`.
124
+ def inside_error_expectation?(node)
125
+ node.each_ancestor(:block).any? do |ancestor|
126
+ error_assertion?(ancestor)
73
127
  end
74
128
  end
75
129
 
76
- def rspec_it_block?(node)
77
- send_node = node.send_node
130
+ def error_assertion?(block)
131
+ send_node = block.send_node
78
132
  return false unless send_node
133
+ return true if send_node.method?(:assert_raises)
134
+
135
+ send_node.method?(:expect) && error_expectation?(block)
136
+ end
137
+
138
+ def error_expectation?(expect_block)
139
+ parent = expect_block.parent
140
+ return false unless parent&.send_type?
141
+ return false unless %i[to not_to].include?(parent.method_name)
142
+
143
+ error_matcher?(parent.first_argument)
144
+ end
145
+
146
+ def error_matcher?(matcher)
147
+ matcher = matcher.send_node if matcher&.block_type?
148
+ matcher&.send_type? &&
149
+ %i[raise_error raise_exception].include?(matcher.method_name)
150
+ end
79
151
 
80
- %i[it specify example].include?(send_node.method_name)
152
+ def component_receiver?(receiver)
153
+ return false unless receiver
154
+ return view_component_parent?(receiver) if receiver.const_type?
155
+ return false unless described_class_reference?(receiver)
156
+
157
+ described_class = enclosing_described_class(receiver)
158
+ described_class && view_component_parent?(described_class)
159
+ end
160
+
161
+ def render_call?(node)
162
+ node.each_node(:send).any? do |send_node|
163
+ RENDER_METHODS.include?(send_node.method_name)
164
+ end
165
+ end
166
+
167
+ # True when the node references a `let`/`subject` helper whose body
168
+ # satisfies the given block.
169
+ def referenced_helper?(node)
170
+ helper_bodies = helper_definitions(node)
171
+
172
+ referenced_helper_names(node).any? do |name|
173
+ body = helper_bodies[name]
174
+ body && yield(body)
175
+ end
81
176
  end
82
177
  end
83
178
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module ViewComponent
5
- VERSION = "0.7.0"
5
+ VERSION = "0.8.0"
6
6
  end
7
7
  end
data/script/verify CHANGED
@@ -49,7 +49,7 @@ def main
49
49
 
50
50
  Dir.chdir(config[:verification_dir]) do
51
51
  configure_rubocop(config[:config_file], config[:display_name])
52
- add_gem_to_gemfile
52
+ replace_gemfile
53
53
 
54
54
  Bundler.with_unbundled_env do
55
55
  output = run_rubocop
@@ -89,33 +89,41 @@ def download_source(dir, tarball_url, display_name)
89
89
  system!("tar", "xz", "-C", dir, "--strip-components=1", "-f", "#{dir}/source.tar.gz")
90
90
  end
91
91
 
92
+ # Replace the library's .rubocop.yml with a config that only runs our
93
+ # ViewComponent cops. The library's own config may reference gems outside the
94
+ # minimal bundle (see replace_gemfile), e.g. inherit_gem, require, or config
95
+ # for cops from other RuboCop extensions.
92
96
  def configure_rubocop(config_file, display_name)
93
- puts "Configuring .rubocop.yml with #{display_name} overrides..."
94
- config = File.exist?(".rubocop.yml") ? YAML.load_file(".rubocop.yml") : {}
95
- library_config = YAML.load_file(config_file)
96
-
97
- # Merge the library config into the existing config
98
- library_config.each do |key, value|
99
- config[key] = if config[key].is_a?(Hash) && value.is_a?(Hash)
100
- config[key].merge(value)
101
- else
102
- value
103
- end
104
- end
97
+ puts "Replacing .rubocop.yml with #{display_name} overrides..."
98
+ config = YAML.load_file(config_file)
99
+
100
+ # Silence warnings about unconfigured new cops - only ViewComponent cops run
101
+ config["AllCops"] ||= {}
102
+ config["AllCops"]["NewCops"] = "disable"
105
103
 
106
104
  File.write(".rubocop.yml", YAML.dump(config))
107
105
  end
108
106
 
109
- def add_gem_to_gemfile
110
- puts "Adding rubocop-view_component gem to Gemfile..."
111
- File.open("Gemfile", "a") { |f| f.puts "gem 'rubocop-view_component', path: '#{GEM_DIR}'" }
107
+ # Replace the library's Gemfile with a minimal one. This avoids installing the
108
+ # library's own dependencies, which are not needed for static analysis and can
109
+ # fail to install or conflict with the rubocop version we require.
110
+ # `view_component` must be present so the project index can resolve
111
+ # `ViewComponent::Base` for ancestry-based detection.
112
+ def replace_gemfile
113
+ puts "Replacing Gemfile with minimal setup..."
114
+ File.write("Gemfile", <<~RUBY)
115
+ source "https://rubygems.org"
116
+
117
+ gem "rubocop"
118
+ gem "rubocop-view_component", path: "#{GEM_DIR}"
119
+ gem "view_component"
120
+ RUBY
112
121
  end
113
122
 
114
123
  def run_rubocop
115
124
  FileUtils.rm_f("Gemfile.lock")
116
125
  puts "Running bundle install..."
117
- system!({ "BUNDLE_WITHOUT" => "development" }, "bundle", "install")
118
- system!("bundle", "add", "rubocop")
126
+ system!("bundle", "install")
119
127
 
120
128
  puts "Running RuboCop (ViewComponent cops only)..."
121
129
  output, status = Open3.capture2(
@@ -128,6 +136,9 @@ def run_rubocop
128
136
 
129
137
  puts "RuboCop exit status: #{status.exitstatus}"
130
138
 
139
+ # Exit status 0 means no offenses, 1 means offenses found (expected, since
140
+ # the libraries have known offenses). 2 means RuboCop itself failed.
141
+ abort "ERROR: RuboCop failed (exit status: #{status.exitstatus})" if status.exitstatus == 2
131
142
  abort "ERROR: RuboCop produced no output (exit status: #{status.exitstatus})" if output.strip.empty?
132
143
 
133
144
  output
@@ -0,0 +1,64 @@
1
+ # This file is auto-generated by `script/verify --regenerate`.
2
+ # It captures known offenses in the library source.
3
+ ---
4
+ ViewComponent/MissingPreview:
5
+ - 'app/components/flowbite/badge/dot.rb'
6
+ - 'app/components/flowbite/badge/pill.rb'
7
+ - 'app/components/flowbite/breadcrumb/home_icon.rb'
8
+ - 'app/components/flowbite/breadcrumb/item/current.rb'
9
+ - 'app/components/flowbite/breadcrumb/item/first.rb'
10
+ - 'app/components/flowbite/breadcrumb/separator_icon.rb'
11
+ - 'app/components/flowbite/button/outline.rb'
12
+ - 'app/components/flowbite/button/pill.rb'
13
+ - 'app/components/flowbite/card/title.rb'
14
+ - 'app/components/flowbite/input/date.rb'
15
+ - 'app/components/flowbite/input/date_time.rb'
16
+ - 'app/components/flowbite/input/email.rb'
17
+ - 'app/components/flowbite/input/file.rb'
18
+ - 'app/components/flowbite/input/hint.rb'
19
+ - 'app/components/flowbite/input/label.rb'
20
+ - 'app/components/flowbite/input/number.rb'
21
+ - 'app/components/flowbite/input/password.rb'
22
+ - 'app/components/flowbite/input/phone.rb'
23
+ - 'app/components/flowbite/input/textarea.rb'
24
+ - 'app/components/flowbite/input/url.rb'
25
+ - 'app/components/flowbite/input/validation_error.rb'
26
+ - 'app/components/flowbite/input_field/date.rb'
27
+ - 'app/components/flowbite/input_field/date_time.rb'
28
+ - 'app/components/flowbite/input_field/email.rb'
29
+ - 'app/components/flowbite/input_field/file.rb'
30
+ - 'app/components/flowbite/input_field/number.rb'
31
+ - 'app/components/flowbite/input_field/password.rb'
32
+ - 'app/components/flowbite/input_field/phone.rb'
33
+ - 'app/components/flowbite/input_field/text.rb'
34
+ - 'app/components/flowbite/input_field/textarea.rb'
35
+ - 'app/components/flowbite/input_field/url.rb'
36
+ - 'app/components/flowbite/pagination/chevron_left_icon.rb'
37
+ - 'app/components/flowbite/pagination/chevron_right_icon.rb'
38
+ - 'app/components/flowbite/pagination/gap.rb'
39
+ - 'app/components/flowbite/pagination/next_link.rb'
40
+ - 'app/components/flowbite/pagination/previous_link.rb'
41
+ - 'app/components/flowbite/sidebar/item.rb'
42
+ - 'app/components/flowbite/sidebar/navigation.rb'
43
+ - 'app/components/flowbite/toast/icon.rb'
44
+ ViewComponent/PreferPrivateMethods:
45
+ - 'app/components/flowbite/badge.rb'
46
+ - 'app/components/flowbite/input.rb'
47
+ - 'app/components/flowbite/input/checkbox.rb'
48
+ - 'app/components/flowbite/input/date.rb'
49
+ - 'app/components/flowbite/input/date_time.rb'
50
+ - 'app/components/flowbite/input/email.rb'
51
+ - 'app/components/flowbite/input/file.rb'
52
+ - 'app/components/flowbite/input/hint.rb'
53
+ - 'app/components/flowbite/input/label.rb'
54
+ - 'app/components/flowbite/input/number.rb'
55
+ - 'app/components/flowbite/input/password.rb'
56
+ - 'app/components/flowbite/input/phone.rb'
57
+ - 'app/components/flowbite/input/radio_button.rb'
58
+ - 'app/components/flowbite/input/select.rb'
59
+ - 'app/components/flowbite/input/url.rb'
60
+ - 'app/components/flowbite/input_field.rb'
61
+ - 'app/components/flowbite/input_field/select.rb'
62
+ - 'app/components/flowbite/pagination/link.rb'
63
+ - 'app/components/flowbite/pagination/next_link.rb'
64
+ - 'app/components/flowbite/pagination/previous_link.rb'
@@ -3,24 +3,20 @@
3
3
  ---
4
4
  ViewComponent/ComponentSuffix:
5
5
  - 'app/components/govuk_component/base.rb'
6
- - 'app/components/govuk_component/header_component.rb'
7
- - 'app/components/govuk_component/notification_banner_component.rb'
6
+ - 'app/components/govuk_component/header_component.rb' # due to nested class
7
+ - 'app/components/govuk_component/language_navigation_component.rb' # due to nested class
8
+ - 'app/components/govuk_component/notification_banner_component.rb' # due to nested class
8
9
  - 'app/components/govuk_component/pagination_component/adjacent_page.rb'
9
10
  - 'app/components/govuk_component/pagination_component/item.rb'
10
- - 'app/components/govuk_component/panel_component.rb'
11
- - 'app/components/govuk_component/tab_component.rb'
11
+ - 'app/components/govuk_component/pagination_component/next_page.rb'
12
+ - 'app/components/govuk_component/pagination_component/previous_page.rb'
13
+ - 'app/components/govuk_component/panel_component.rb' # due to nested class
14
+ - 'app/components/govuk_component/tab_component.rb' # due to nested class
12
15
  ViewComponent/PreferPrivateMethods:
13
16
  - 'app/components/govuk_component/base.rb'
17
+ - 'app/components/govuk_component/language_navigation_component.rb'
14
18
  - 'app/components/govuk_component/notification_banner_component.rb'
19
+ - 'app/components/govuk_component/pagination_component/next_page.rb'
15
20
  - 'app/components/govuk_component/panel_component.rb'
16
21
  - 'app/components/govuk_component/service_navigation_component.rb'
17
22
  - 'app/components/govuk_component/tab_component.rb'
18
- ViewComponent/TestRenderedOutput:
19
- - 'spec/components/govuk_component/accordion_component_spec.rb'
20
- - 'spec/components/govuk_component/breadcrumbs_component_spec.rb'
21
- - 'spec/components/govuk_component/configuration/footer_component_configuration_spec.rb'
22
- - 'spec/components/govuk_component/footer_component_spec.rb'
23
- - 'spec/components/govuk_component/notification_banner_component_spec.rb'
24
- - 'spec/components/govuk_component/pagination_component_spec.rb'
25
- - 'spec/components/govuk_component/task_list_component_spec.rb'
26
- - 'spec/components/govuk_component/warning_text_component_spec.rb'
@@ -17,6 +17,8 @@ ViewComponent/MissingPreview:
17
17
  - 'app/components/polaris/inline_code_component.rb'
18
18
  - 'app/components/polaris/label_component.rb'
19
19
  - 'app/components/polaris/labelled_component.rb'
20
+ - 'app/components/polaris/layout/annotated_section.rb'
21
+ - 'app/components/polaris/layout/section.rb'
20
22
  - 'app/components/polaris/list_component.rb'
21
23
  - 'app/components/polaris/page_component.rb'
22
24
  - 'app/components/polaris/placeholder_component.rb'
@@ -2,9 +2,66 @@
2
2
  # It captures known offenses in the library source.
3
3
  ---
4
4
  ViewComponent/MissingPreview:
5
+ - 'app/components/primer/alpha/action_bar/divider.rb'
6
+ - 'app/components/primer/alpha/action_bar/item.rb'
7
+ - 'app/components/primer/alpha/action_list/form_wrapper.rb'
8
+ - 'app/components/primer/alpha/action_menu/group.rb'
9
+ - 'app/components/primer/alpha/action_menu/heading.rb'
10
+ - 'app/components/primer/alpha/action_menu/list.rb'
11
+ - 'app/components/primer/alpha/action_menu/list_wrapper.rb'
12
+ - 'app/components/primer/alpha/auto_complete/item.rb'
13
+ - 'app/components/primer/alpha/dialog/body.rb'
14
+ - 'app/components/primer/alpha/dialog/footer.rb'
15
+ - 'app/components/primer/alpha/dialog/header.rb'
16
+ - 'app/components/primer/alpha/dropdown/menu.rb'
17
+ - 'app/components/primer/alpha/file_tree_view/directory_node.rb'
18
+ - 'app/components/primer/alpha/file_tree_view/file_node.rb'
19
+ - 'app/components/primer/alpha/form_button.rb'
20
+ - 'app/components/primer/alpha/image.rb'
21
+ - 'app/components/primer/alpha/layout.rb'
22
+ - 'app/components/primer/alpha/nav_list.rb'
23
+ - 'app/components/primer/alpha/nav_list/divider.rb'
24
+ - 'app/components/primer/alpha/nav_list/group.rb'
25
+ - 'app/components/primer/alpha/nav_list/heading.rb'
26
+ - 'app/components/primer/alpha/nav_list/item.rb'
27
+ - 'app/components/primer/alpha/overlay/body.rb'
28
+ - 'app/components/primer/alpha/overlay/footer.rb'
29
+ - 'app/components/primer/alpha/overlay/header.rb'
30
+ - 'app/components/primer/alpha/segmented_control/item.rb'
31
+ - 'app/components/primer/alpha/select_panel.rb'
32
+ - 'app/components/primer/alpha/submit_button.rb'
33
+ - 'app/components/primer/alpha/tab_container.rb'
34
+ - 'app/components/primer/alpha/tree_view/icon.rb'
35
+ - 'app/components/primer/alpha/tree_view/icon_pair.rb'
36
+ - 'app/components/primer/alpha/tree_view/leading_action.rb'
37
+ - 'app/components/primer/alpha/tree_view/loading_failure_message.rb'
38
+ - 'app/components/primer/alpha/tree_view/node.rb'
39
+ - 'app/components/primer/alpha/tree_view/skeleton_loader.rb'
40
+ - 'app/components/primer/alpha/tree_view/spinner_loader.rb'
41
+ - 'app/components/primer/alpha/tree_view/sub_tree.rb'
42
+ - 'app/components/primer/alpha/tree_view/sub_tree_container.rb'
43
+ - 'app/components/primer/alpha/tree_view/visual.rb'
44
+ - 'app/components/primer/beta/auto_complete/item.rb'
45
+ - 'app/components/primer/beta/auto_complete/no_result_item.rb'
46
+ - 'app/components/primer/beta/border_box/header.rb'
47
+ - 'app/components/primer/beta/breadcrumbs.rb'
48
+ - 'app/components/primer/beta/button_group.rb'
49
+ - 'app/components/primer/beta/clipboard_copy_button.rb'
50
+ - 'app/components/primer/beta/timeline_item.rb'
51
+ - 'app/components/primer/beta/truncate.rb'
5
52
  - 'app/components/primer/blankslate_component.rb'
6
53
  - 'app/components/primer/button_component.rb'
54
+ - 'app/components/primer/conditional_wrapper.rb'
55
+ - 'app/components/primer/content.rb'
56
+ - 'app/components/primer/icon_button.rb'
7
57
  - 'app/components/primer/layout_component.rb'
58
+ - 'app/components/primer/tooltip.rb'
59
+ - 'app/components/primer/truncate.rb'
60
+ - 'test/components/beta/nav_list_test.rb'
61
+ - 'test/lib/primer/audited/dsl_test.rb'
62
+ - 'test/lib/primer/forms/form_test_component.rb'
63
+ - 'test/lib/primer/status/dsl_test.rb'
64
+ - 'test/test_helpers/components/primer/deny_component.rb'
8
65
  ViewComponent/PreferPrivateMethods:
9
66
  - 'app/components/primer/alpha/action_list.rb'
10
67
  - 'app/components/primer/alpha/action_list/divider.rb'
@@ -18,5 +75,3 @@ ViewComponent/PreferPrivateMethods:
18
75
  - 'app/components/primer/beta/nav_list/divider.rb'
19
76
  - 'app/components/primer/beta/nav_list/group.rb'
20
77
  - 'app/components/primer/beta/nav_list/item.rb'
21
- ViewComponent/TestRenderedOutput:
22
- - 'test/components/alpha/action_list_test.rb'
@@ -89,4 +89,20 @@ RSpec.describe RuboCop::Cop::ViewComponent::Base do
89
89
  )
90
90
  end
91
91
  end
92
+
93
+ describe "#resolved_index_declaration" do
94
+ it "raises when project_index is not set" do
95
+ source = parse_source(<<~RUBY)
96
+ class Foo < ViewComponent::Base
97
+ end
98
+ RUBY
99
+
100
+ parent = source.ast.parent_class
101
+
102
+ expect { cop.send(:resolved_index_declaration, parent) }.to raise_error(
103
+ RuntimeError,
104
+ /UseProjectIndex/
105
+ )
106
+ end
107
+ end
92
108
  end