rubocop-view_component 0.6.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be0a1c1c9a7489ed311953b5f67c7108dbca8b1f43debef36026ec011781a549
4
- data.tar.gz: 0b62b04534ab81725ab96d28c0b1931671d92944277ac7b5217f4d36d9d91f9b
3
+ metadata.gz: fa0434208243f306c17f51b681abdf59f6152d6afdd0014c608c5b4e08e893c2
4
+ data.tar.gz: e709241be864e024f8ce410b6620d62bcdd12cd090e2593b378c0169a628f1e9
5
5
  SHA512:
6
- metadata.gz: 7fc23bef50c35486b422362f9e738e5347b41eaeb8171f2dfca21884dbd87542f2db1e22618c1b239a1bfd2d878b448cb208539678c37b6e622086c1ee4d2871
7
- data.tar.gz: f67e8fc0ec682da84318f7078bc5eaffd4a7e9907e9d4e281a224e240e57a13b4f4ec010ef31cce3f20528b4f6fc4ce5acf538385736a837095ef21a90fcf641
6
+ metadata.gz: 6d0a9815a58ab3326c9366e7abb2d85cd18e1c87216c731c2fccfeba3d70eae2188856b1c619ba9be2d37c776d463d00efe4f2b7d680998e6e7679b582ab22da
7
+ data.tar.gz: f195f94ca71e89a4f807a8074ddac30a65462faab27dc01b18f1f58f4a29d85df5699a4dcead7a670e7f311b3fe87816cd49ae98b3813540fa2e4c0707273fc8
data/.rubocop.yml CHANGED
@@ -16,6 +16,7 @@ AllCops:
16
16
  - 'verification/**/*'
17
17
  - 'script/verify'
18
18
  - 'spec/fixtures/**/*'
19
+ - 'gemfiles/**/*'
19
20
 
20
21
  Metrics/BlockLength:
21
22
  Exclude:
data/Appraisals ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ appraise "rubocop-minimum" do
4
+ gem "rubocop", "= 1.89.0"
5
+ end
data/CLAUDE.md CHANGED
@@ -40,8 +40,10 @@ All cops inherit from `RuboCop::Cop::Base` and are located in `lib/rubocop/cop/v
40
40
  ### Shared Modules
41
41
 
42
42
  **`ViewComponent::Base`** (`lib/rubocop/cop/view_component/base.rb`)
43
- - Provides `view_component_class?(node)` - Detects ViewComponent classes
44
- - Provides `view_component_parent?(node)` - Checks if inheriting from ViewComponent::Base, ApplicationComponent, or configured parent classes
43
+ - Includes `RuboCop::Cop::ProjectIndexHelp` for rubydex integration
44
+ - Provides `view_component_class?(node)` - Detects ViewComponent classes via ancestry resolution
45
+ - Provides `view_component_parent?(node)` - Checks if inheriting from ViewComponent::Base
46
+ - Provides `view_component_parent_class?(node)` - Checks if a class is an abstract parent (has descendants)
45
47
  - Provides `inside_view_component?(node)` - Checks if code is within a ViewComponent
46
48
 
47
49
  **`TemplateAnalyzer`** (`lib/rubocop/cop/view_component/template_analyzer.rb`)
@@ -50,16 +52,6 @@ All cops inherit from `RuboCop::Cop::Base` and are located in `lib/rubocop/cop/v
50
52
  - Handles both sibling templates (`component.html.erb`) and sidecar templates (`component/component.html.erb`)
51
53
  - Uses the `herb` gem to parse ERB and extract Ruby code
52
54
 
53
- ### Configuration
54
-
55
- The `ViewComponent` config supports `ViewComponentParentClasses` to configure additional base classes beyond `ViewComponent::Base` and `ApplicationComponent`:
56
-
57
- ```yaml
58
- ViewComponent:
59
- ViewComponentParentClasses:
60
- - MyApp::BaseComponent
61
- ```
62
-
63
55
  ### Verification System
64
56
 
65
57
  The `script/verify` script downloads real component libraries, runs all ViewComponent cops, and compares results to checked-in snapshots. This catches regressions when cop behavior changes. Libraries are configured in `verification/libraries.yml`, downloaded to `verification/LIBRARY/`, and expected results stored in `spec/expected_LIBRARY_failures.yml`.
data/README.md CHANGED
@@ -16,6 +16,16 @@ Add to your `.rubocop.yml`:
16
16
  require:
17
17
  - rubocop-view_component
18
18
  ```
19
+ **Note:** From v0.7.0 onwards, this gem makes use of [rubydex](https://github.com/Shopify/rubydex) to discover which classes inherit from `ViewComponent::Base`. `UseProjectIndex` is required. Without it, cops that depend on ancestry detection will raise an error.
20
+
21
+ ```yaml
22
+ AllCops:
23
+ UseProjectIndex: true
24
+ ```
25
+
26
+ If you use a ViewComponent library such as [primer](https://github.com/primer/view_components) then you'll also need to set `ProjectIndexIncludesGems: true` so the cops can resolve ancestry chains that pass through gem classes.
27
+
28
+ For more background on how RuboCop uses rubydex for cross-file analysis, see [RuboCop 1.89: Project-Wide Analysis with Rubydex](https://metaredux.com/posts/2026/08/05/rubocop-1-89.html).
19
29
 
20
30
  ## Cops
21
31
 
@@ -27,34 +37,10 @@ This gem provides several cops to enforce ViewComponent best practices:
27
37
  - **ViewComponent/PreferSlots** - Detect HTML parameters that should be slots
28
38
  - **ViewComponent/PreferComposition** - Avoid inheriting one ViewComponent from another (prefer composition)
29
39
  - **ViewComponent/TestRenderedOutput** - Encourage testing rendered output over private methods
30
- - **ViewComponent/MissingPreview** - Ensure every ViewComponent has a corresponding preview file (requires `PreviewPaths` configuration). Classes listed in `ViewComponentParentClasses` are automatically exempt, as abstract base classes are not rendered standalone.
40
+ - **ViewComponent/MissingPreview** - Ensure every ViewComponent has a corresponding preview file (requires `PreviewPaths` configuration). Abstract base classes with descendants are automatically exempt.
31
41
 
32
42
  ## Optional Configuration
33
43
 
34
- ### Base Class
35
-
36
- By default, the cops detect classes that inherit from `ViewComponent::Base` or `ApplicationComponent`. To add extra parent classes, use `inherit_mode: merge` so the defaults are preserved:
37
-
38
- ```yaml
39
- # .rubocop.yml
40
- ViewComponent:
41
- inherit_mode:
42
- merge:
43
- - ViewComponentParentClasses
44
- ViewComponentParentClasses:
45
- - MyApp::BaseComponent
46
- ```
47
-
48
- To replace the defaults entirely (e.g. if your project has renamed `ApplicationComponent`), omit `inherit_mode`:
49
-
50
- ```yaml
51
- # .rubocop.yml
52
- ViewComponent:
53
- ViewComponentParentClasses:
54
- - ViewComponent::Base
55
- - MyApp::BaseComponent
56
- ```
57
-
58
44
  ### Components Directory
59
45
 
60
46
  Several cops (`ComponentSuffix`, `PreferComposition`, `MissingPreview`, `TestRenderedOutput`) default to running only on files under `app/components/`. If your project uses a different path, override `Include` in your `.rubocop.yml`:
data/config/default.yml CHANGED
@@ -1,7 +1,4 @@
1
1
  ViewComponent:
2
- ViewComponentParentClasses:
3
- - ViewComponent::Base
4
- - ApplicationComponent
5
2
  ComponentNamespaces: []
6
3
 
7
4
  ViewComponent/ComponentSuffix:
@@ -0,0 +1,13 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "appraisal"
6
+ gem "irb"
7
+ gem "rake", "~> 13.4"
8
+ gem "rspec"
9
+ gem "rubocop", "= 1.89.0"
10
+ gem "rubocop-rake"
11
+ gem "rubocop-rspec"
12
+
13
+ gemspec path: "../"
@@ -0,0 +1,162 @@
1
+ PATH
2
+ remote: ..
3
+ specs:
4
+ rubocop-view_component (0.6.0)
5
+ activesupport
6
+ herb
7
+ lint_roller (~> 1.1)
8
+ parser
9
+ rubocop (>= 1.89.0)
10
+ rubydex
11
+
12
+ GEM
13
+ remote: https://rubygems.org/
14
+ specs:
15
+ activesupport (8.1.3.1)
16
+ base64
17
+ bigdecimal
18
+ concurrent-ruby (~> 1.0, >= 1.3.1)
19
+ connection_pool (>= 2.2.5)
20
+ drb
21
+ i18n (>= 1.6, < 2)
22
+ json
23
+ logger (>= 1.4.2)
24
+ minitest (>= 5.1)
25
+ securerandom (>= 0.3)
26
+ tzinfo (~> 2.0, >= 2.0.5)
27
+ uri (>= 0.13.1)
28
+ appraisal (2.5.0)
29
+ bundler
30
+ rake
31
+ thor (>= 0.14.0)
32
+ ast (2.4.3)
33
+ base64 (0.3.0)
34
+ bigdecimal (4.1.2)
35
+ concurrent-ruby (1.3.8)
36
+ connection_pool (3.0.2)
37
+ diff-lcs (1.6.2)
38
+ drb (2.2.3)
39
+ erb (6.0.7)
40
+ herb (0.10.3)
41
+ herb (0.10.3-aarch64-linux-gnu)
42
+ herb (0.10.3-aarch64-linux-musl)
43
+ herb (0.10.3-arm-linux-gnu)
44
+ herb (0.10.3-arm-linux-musl)
45
+ herb (0.10.3-arm64-darwin)
46
+ herb (0.10.3-x86-linux-gnu)
47
+ herb (0.10.3-x86-linux-musl)
48
+ herb (0.10.3-x86_64-darwin)
49
+ herb (0.10.3-x86_64-linux-gnu)
50
+ herb (0.10.3-x86_64-linux-musl)
51
+ i18n (1.15.2)
52
+ concurrent-ruby (~> 1.0)
53
+ io-console (0.9.1)
54
+ irb (1.18.0)
55
+ pp (>= 0.6.0)
56
+ prism (>= 1.3.0)
57
+ rdoc (>= 4.0.0)
58
+ reline (>= 0.4.2)
59
+ json (2.21.2)
60
+ language_server-protocol (3.17.0.6)
61
+ lint_roller (1.1.0)
62
+ logger (1.7.0)
63
+ minitest (6.0.6)
64
+ drb (~> 2.0)
65
+ prism (~> 1.5)
66
+ parallel (1.28.0)
67
+ parser (3.3.12.0)
68
+ ast (~> 2.4.1)
69
+ racc
70
+ pp (0.6.4)
71
+ prettyprint
72
+ prettyprint (0.2.0)
73
+ prism (1.9.0)
74
+ racc (1.8.1)
75
+ rainbow (3.1.1)
76
+ rake (13.4.2)
77
+ rbs (4.1.2)
78
+ logger
79
+ prism (>= 1.6.0)
80
+ tsort
81
+ rdoc (8.0.0)
82
+ erb
83
+ prism (>= 1.6.0)
84
+ rbs (>= 4.0.0)
85
+ tsort
86
+ regexp_parser (2.12.0)
87
+ reline (0.7.0)
88
+ io-console (~> 0.5)
89
+ rspec (3.13.2)
90
+ rspec-core (~> 3.13.0)
91
+ rspec-expectations (~> 3.13.0)
92
+ rspec-mocks (~> 3.13.0)
93
+ rspec-core (3.13.6)
94
+ rspec-support (~> 3.13.0)
95
+ rspec-expectations (3.13.5)
96
+ diff-lcs (>= 1.2.0, < 2.0)
97
+ rspec-support (~> 3.13.0)
98
+ rspec-mocks (3.13.8)
99
+ diff-lcs (>= 1.2.0, < 2.0)
100
+ rspec-support (~> 3.13.0)
101
+ rspec-support (3.13.7)
102
+ rubocop (1.89.0)
103
+ json (~> 2.3)
104
+ language_server-protocol (~> 3.17.0.2)
105
+ lint_roller (~> 1.1.0)
106
+ parallel (>= 1.10)
107
+ parser (>= 3.3.0.2)
108
+ rainbow (>= 2.2.2, < 4.0)
109
+ regexp_parser (>= 2.9.3, < 3.0)
110
+ rubocop-ast (>= 1.49.0, < 2.0)
111
+ ruby-progressbar (~> 1.7)
112
+ unicode-display_width (>= 2.4.0, < 4.0)
113
+ rubocop-ast (1.50.0)
114
+ parser (>= 3.3.7.2)
115
+ prism (~> 1.7)
116
+ rubocop-rake (0.7.1)
117
+ lint_roller (~> 1.1)
118
+ rubocop (>= 1.72.1)
119
+ rubocop-rspec (3.7.0)
120
+ lint_roller (~> 1.1)
121
+ rubocop (~> 1.72, >= 1.72.1)
122
+ ruby-progressbar (1.13.0)
123
+ rubydex (0.3.0)
124
+ rubydex (0.3.0-aarch64-linux)
125
+ rubydex (0.3.0-arm64-darwin)
126
+ rubydex (0.3.0-x86_64-darwin)
127
+ rubydex (0.3.0-x86_64-linux)
128
+ securerandom (0.4.1)
129
+ thor (1.5.0)
130
+ tsort (0.2.0)
131
+ tzinfo (2.0.6)
132
+ concurrent-ruby (~> 1.0)
133
+ unicode-display_width (3.2.0)
134
+ unicode-emoji (~> 4.1)
135
+ unicode-emoji (4.2.0)
136
+ uri (1.1.1)
137
+
138
+ PLATFORMS
139
+ aarch64-linux-gnu
140
+ aarch64-linux-musl
141
+ arm-linux-gnu
142
+ arm-linux-musl
143
+ arm64-darwin
144
+ ruby
145
+ x86-linux-gnu
146
+ x86-linux-musl
147
+ x86_64-darwin
148
+ x86_64-linux-gnu
149
+ x86_64-linux-musl
150
+
151
+ DEPENDENCIES
152
+ appraisal
153
+ irb
154
+ rake (~> 13.4)
155
+ rspec
156
+ rubocop (= 1.89.0)
157
+ rubocop-rake
158
+ rubocop-rspec
159
+ rubocop-view_component!
160
+
161
+ BUNDLED WITH
162
+ 2.7.2
@@ -1,35 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rubydex"
4
+
3
5
  module RuboCop
4
6
  module Cop
5
7
  module ViewComponent
6
- # Shared helper methods for ViewComponent cops
8
+ # Shared helper methods for ViewComponent cops.
9
+ # Requires rubydex with UseProjectIndex: true for ancestry resolution.
7
10
  module Base
8
- # Check if a class node inherits from ViewComponent::Base or ApplicationComponent
11
+ include ProjectIndexHelp
12
+
13
+ VC_BASE = "ViewComponent::Base"
14
+
15
+ # Check if a class node inherits from ViewComponent::Base
9
16
  def view_component_class?(node)
10
17
  return false unless node&.class_type?
11
18
 
12
19
  class_source = fully_qualified_name(node)
13
20
  return true if component_namespaces.any? { |ns| class_source.start_with?(ns) }
14
21
 
22
+ require_project_index!
23
+
15
24
  parent_class = node.parent_class
16
25
  return false unless parent_class
17
26
 
18
27
  view_component_parent?(parent_class)
19
28
  end
20
29
 
21
- # Check if node represents a configured parent class
30
+ # Check if a constant node resolves to a class with ViewComponent::Base as ancestor
22
31
  def view_component_parent?(node)
23
32
  return false unless node.const_type?
24
33
 
25
- (cop_config["ViewComponentParentClasses"] || []).include?(node.source)
34
+ require_project_index!
35
+
36
+ declaration = resolve_constant_in_index(node)
37
+ return false unless declaration.respond_to?(:has_ancestor?)
38
+
39
+ declaration.name == VC_BASE || declaration.has_ancestor?(VC_BASE)
26
40
  end
27
41
 
28
- # Check if a class node is itself one of the registered parent classes.
42
+ # Check if a class node is itself an abstract parent class
43
+ # (has ViewComponent::Base as ancestor and has descendants in the project)
29
44
  def view_component_parent_class?(node)
30
45
  return false unless node&.class_type?
31
46
 
32
- (cop_config["ViewComponentParentClasses"] || []).include?(fully_qualified_name(node))
47
+ require_project_index!
48
+
49
+ declaration = resolve_constant_in_index(node.identifier)
50
+ return false unless declaration.respond_to?(:has_ancestor?)
51
+
52
+ declaration.has_ancestor?(VC_BASE) &&
53
+ declaration.descendants.any? { |d| d.name != declaration.name }
33
54
  end
34
55
 
35
56
  def fully_qualified_name(node)
@@ -51,11 +72,22 @@ module RuboCop
51
72
 
52
73
  # Check if node is within a ViewComponent class
53
74
  def inside_view_component?(node)
75
+ require_project_index!
76
+
54
77
  klass = enclosing_class(node)
55
78
  return false unless klass
56
79
 
57
80
  view_component_class?(klass)
58
81
  end
82
+
83
+ def require_project_index!
84
+ return if project_index
85
+
86
+ raise <<~MSG
87
+ ViewComponent cops require `AllCops/UseProjectIndex: true` in your .rubocop.yml
88
+ for ancestry-based class detection.
89
+ MSG
90
+ end
59
91
  end
60
92
  end
61
93
  end
@@ -47,6 +47,7 @@ module RuboCop
47
47
  add_offense(node)
48
48
  end
49
49
  alias on_numblock on_block
50
+ alias on_itblock on_block
50
51
 
51
52
  private
52
53
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module ViewComponent
5
- VERSION = "0.6.0"
5
+ VERSION = "0.7.0"
6
6
  end
7
7
  end
data/script/verify CHANGED
@@ -112,8 +112,10 @@ 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(
@@ -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:
@@ -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,64 +74,6 @@ RSpec.describe RuboCop::Cop::ViewComponent::ComponentSuffix, :config do
74
74
  end
75
75
  end
76
76
 
77
- context "when ViewComponentParentClasses is configured with merge" do
78
- let(:config) do
79
- RuboCop::Config.new(
80
- "ViewComponent/ComponentSuffix" => {
81
- "ViewComponentParentClasses" => ["ViewComponent::Base", "ApplicationComponent", "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
- context "when ViewComponentParentClasses is configured replacing defaults" do
111
- let(:config) do
112
- RuboCop::Config.new(
113
- "ViewComponent/ComponentSuffix" => {
114
- "ViewComponentParentClasses" => ["Primer::Component"]
115
- }
116
- )
117
- end
118
-
119
- it "registers an offense for classes inheriting from a configured parent" do
120
- expect_offense(<<~RUBY)
121
- class FooBar < Primer::Component
122
- ^^^^^^ ViewComponent class names should end with `Component`.
123
- end
124
- RUBY
125
- end
126
-
127
- it "does not recognize default parent classes that were replaced" do
128
- expect_no_offenses(<<~RUBY)
129
- class FooBar < ViewComponent::Base
130
- end
131
- RUBY
132
- end
133
- end
134
-
135
77
  context "with compact nested class syntax" do
136
78
  it "registers offense for compact syntax" do
137
79
  expect_offense(<<~RUBY)
@@ -5,8 +5,7 @@ RSpec.describe RuboCop::Cop::ViewComponent::MissingPreview, :config do
5
5
  RuboCop::Config.new(
6
6
  "ViewComponent/MissingPreview" => {
7
7
  "Enabled" => true,
8
- "PreviewPaths" => ["/previews"],
9
- "ViewComponentParentClasses" => %w[ViewComponent::Base ApplicationComponent]
8
+ "PreviewPaths" => ["/previews"]
10
9
  }
11
10
  )
12
11
  end
@@ -172,45 +171,5 @@ RSpec.describe RuboCop::Cop::ViewComponent::MissingPreview, :config do
172
171
  end
173
172
  RUBY
174
173
  end
175
-
176
- context "when a custom parent class is configured" do
177
- let(:config) do
178
- RuboCop::Config.new(
179
- "ViewComponent/MissingPreview" => {
180
- "Enabled" => true,
181
- "PreviewPaths" => ["/previews"],
182
- "ViewComponentParentClasses" => %w[ViewComponent::Base ApplicationComponent BaseComponent]
183
- }
184
- )
185
- end
186
-
187
- it "does not register an offense for the custom parent class" do
188
- expect_no_offenses(<<~RUBY, "/app/components/base_component.rb")
189
- class BaseComponent < ViewComponent::Base
190
- end
191
- RUBY
192
- end
193
- end
194
-
195
- context "when a namespaced custom parent class is configured" do
196
- let(:config) do
197
- RuboCop::Config.new(
198
- "ViewComponent/MissingPreview" => {
199
- "Enabled" => true,
200
- "PreviewPaths" => ["/previews"],
201
- "ViewComponentParentClasses" => %w[ViewComponent::Base ApplicationComponent MyApp::BaseComponent]
202
- }
203
- )
204
- end
205
-
206
- it "does not register an offense for the namespaced custom parent class" do
207
- expect_no_offenses(<<~RUBY, "/app/components/my_app/base_component.rb")
208
- module MyApp
209
- class BaseComponent < ViewComponent::Base
210
- end
211
- end
212
- RUBY
213
- end
214
- end
215
174
  end
216
175
  end
@@ -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
@@ -147,8 +147,7 @@ RSpec.describe RuboCop::Cop::ViewComponent::PreferPrivateMethods, :config do
147
147
  "AllCops" => { "DisplayCopNames" => true },
148
148
  "ViewComponent/PreferPrivateMethods" => {
149
149
  "AllowedPublicMethods" => %w[initialize call],
150
- "AllowedPublicMethodPatterns" => ["^render_", "^with_"],
151
- "ViewComponentParentClasses" => %w[ViewComponent::Base ApplicationComponent]
150
+ "AllowedPublicMethodPatterns" => ["^render_", "^with_"]
152
151
  }
153
152
  )
154
153
  end
@@ -190,8 +189,7 @@ RSpec.describe RuboCop::Cop::ViewComponent::PreferPrivateMethods, :config do
190
189
  "AllCops" => { "DisplayCopNames" => true },
191
190
  "ViewComponent/PreferPrivateMethods" => {
192
191
  "AllowedPublicMethods" => %w[initialize call custom_public_method],
193
- "AllowedPublicMethodPatterns" => [],
194
- "ViewComponentParentClasses" => %w[ViewComponent::Base ApplicationComponent]
192
+ "AllowedPublicMethodPatterns" => []
195
193
  }
196
194
  )
197
195
  end
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,9 +1,6 @@
1
- ViewComponent:
2
- inherit_mode:
3
- merge:
4
- - ViewComponentParentClasses
5
- ViewComponentParentClasses:
6
- - GovukComponent::Base
1
+ AllCops:
2
+ UseProjectIndex: true
3
+ ProjectIndexIncludesGems: true
7
4
 
8
5
  # x-govuk components do not use ViewComponent previews
9
6
  ViewComponent/MissingPreview:
@@ -1,10 +1,6 @@
1
- ViewComponent:
2
- inherit_mode:
3
- merge:
4
- - ViewComponentParentClasses
5
- ViewComponentParentClasses:
6
- - Polaris::Component
7
- - Component
1
+ AllCops:
2
+ UseProjectIndex: true
3
+ ProjectIndexIncludesGems: true
8
4
 
9
5
  ViewComponent/MissingPreview:
10
6
  PreviewPaths:
@@ -1,10 +1,6 @@
1
- ViewComponent:
2
- inherit_mode:
3
- merge:
4
- - ViewComponentParentClasses
5
- ViewComponentParentClasses:
6
- - Primer::Component
7
- - Primer::BaseComponent
1
+ AllCops:
2
+ UseProjectIndex: true
3
+ ProjectIndexIncludesGems: true
8
4
 
9
5
  # Primer seems to intentionally omit the Component suffix from most class names
10
6
  # (116 out of 131 components), so this cop is not useful here.
@@ -31,3 +27,5 @@ ViewComponent/MissingPreview:
31
27
  - previews
32
28
  Exclude:
33
29
  - app/components/primer/base_component.rb
30
+ # Deprecated wrapper with no preview
31
+ - app/components/primer/navigation/tab_component.rb
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-view_component
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Waite
@@ -65,20 +65,34 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rubydex
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: rubocop
70
84
  requirement: !ruby/object:Gem::Requirement
71
85
  requirements:
72
86
  - - ">="
73
87
  - !ruby/object:Gem::Version
74
- version: 1.72.2
88
+ version: 1.89.0
75
89
  type: :runtime
76
90
  prerelease: false
77
91
  version_requirements: !ruby/object:Gem::Requirement
78
92
  requirements:
79
93
  - - ">="
80
94
  - !ruby/object:Gem::Version
81
- version: 1.72.2
95
+ version: 1.89.0
82
96
  description: A RuboCop extension that enforces ViewComponent best practices and conventions
83
97
  email:
84
98
  - andyw8@users.noreply.github.com
@@ -88,11 +102,14 @@ extra_rdoc_files: []
88
102
  files:
89
103
  - ".rspec"
90
104
  - ".rubocop.yml"
105
+ - Appraisals
91
106
  - CLAUDE.md
92
107
  - LICENSE.txt
93
108
  - README.md
94
109
  - Rakefile
95
110
  - config/default.yml
111
+ - gemfiles/rubocop_minimum.gemfile
112
+ - gemfiles/rubocop_minimum.gemfile.lock
96
113
  - lib/rubocop-view_component.rb
97
114
  - lib/rubocop/cop/view_component/base.rb
98
115
  - lib/rubocop/cop/view_component/component_suffix.rb
@@ -113,6 +130,7 @@ files:
113
130
  - spec/expected_primer_failures.yml
114
131
  - spec/fixtures/components/template_method_component.html.erb
115
132
  - spec/fixtures/components/template_method_component.rb
133
+ - spec/rubocop/cop/view_component/base_spec.rb
116
134
  - spec/rubocop/cop/view_component/component_suffix_spec.rb
117
135
  - spec/rubocop/cop/view_component/missing_preview_spec.rb
118
136
  - spec/rubocop/cop/view_component/no_global_state_spec.rb
@@ -121,6 +139,7 @@ files:
121
139
  - spec/rubocop/cop/view_component/prefer_slots_spec.rb
122
140
  - spec/rubocop/cop/view_component/test_rendered_output_spec.rb
123
141
  - spec/spec_helper.rb
142
+ - spec/support/project_index_helpers.rb
124
143
  - verification/govuk_rubocop_config.yml
125
144
  - verification/libraries.yml
126
145
  - verification/polaris_rubocop_config.yml
@@ -147,7 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
166
  - !ruby/object:Gem::Version
148
167
  version: '0'
149
168
  requirements: []
150
- rubygems_version: 3.6.9
169
+ rubygems_version: 4.0.16
151
170
  specification_version: 4
152
171
  summary: RuboCop extension for ViewComponent best practices
153
172
  test_files: []