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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93f6d112da7bf592a3d0a114d492211798067caea8c8286feee1930d2b56810d
4
- data.tar.gz: b5bb3fcbf1e270cff0410dd743145130ba1de2089374c6f4c6b0618037e64e92
3
+ metadata.gz: fa0434208243f306c17f51b681abdf59f6152d6afdd0014c608c5b4e08e893c2
4
+ data.tar.gz: e709241be864e024f8ce410b6620d62bcdd12cd090e2593b378c0169a628f1e9
5
5
  SHA512:
6
- metadata.gz: 92322d18a8ced7cb42d053bbd595e926b11059a49162ae7c7b3e01721397d45b61ca4b00715955b808b1699fa4d8d2ecdc05a68f6f0114e62b32081a92e2b174
7
- data.tar.gz: d55bb666487e1782dfce9298d78a920c989c58c6c33fc34a7f47f4bddae52543e05741b1f18da1ecbb2fa847345d09cab01c23ef759958204982ac27ee889f8a
6
+ metadata.gz: 6d0a9815a58ab3326c9366e7abb2d85cd18e1c87216c731c2fccfeba3d70eae2188856b1c619ba9be2d37c776d463d00efe4f2b7d680998e6e7679b582ab22da
7
+ data.tar.gz: f195f94ca71e89a4f807a8074ddac30a65462faab27dc01b18f1f58f4a29d85df5699a4dcead7a670e7f311b3fe87816cd49ae98b3813540fa2e4c0707273fc8
data/.rubocop.yml CHANGED
@@ -1,7 +1,40 @@
1
1
  plugins:
2
2
  - rubocop-internal_affairs
3
+ - rubocop-rake
4
+ - rubocop-rspec
3
5
  - rubocop-view_component
4
6
 
7
+ inherit_mode:
8
+ merge:
9
+ - Exclude
10
+
11
+ AllCops:
12
+ TargetRubyVersion: 3.2
13
+ NewCops: enable
14
+ SuggestExtensions: false
15
+ Exclude:
16
+ - 'verification/**/*'
17
+ - 'script/verify'
18
+ - 'spec/fixtures/**/*'
19
+ - 'gemfiles/**/*'
20
+
21
+ Metrics/BlockLength:
22
+ Exclude:
23
+ - 'spec/**/*'
24
+ - '*.gemspec'
25
+
26
+ RSpec/ExampleLength:
27
+ Max: 20
28
+
29
+ Style/StringLiterals:
30
+ EnforcedStyle: double_quotes
31
+
32
+ Style/StringLiteralsInInterpolation:
33
+ EnforcedStyle: double_quotes
34
+
35
+ InternalAffairs/RedundantLetRuboCopConfigNew:
36
+ Enabled: false
37
+
5
38
  Naming/FileName:
6
39
  Exclude:
7
40
  - lib/rubocop-view_component.rb
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
@@ -11,7 +11,7 @@ This is a RuboCop extension that enforces ViewComponent best practices. It provi
11
11
  ### Testing
12
12
  - `rake spec` - Run all RSpec tests
13
13
  - `bundle exec rspec spec/rubocop/cop/view_component/FILENAME_spec.rb` - Run a specific spec file
14
- - `rake standard` - Run Standard (RuboCop) linting
14
+ - `rake rubocop` - Run RuboCop linting
15
15
  - `rake` - Run both tests and linting (default task)
16
16
 
17
17
  ### Verification
@@ -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,19 +52,9 @@ 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 `AllCops` config supports `ViewComponentParentClasses` to configure additional base classes beyond `ViewComponent::Base` and `ApplicationComponent`:
56
-
57
- ```yaml
58
- AllCops:
59
- ViewComponentParentClasses:
60
- - MyApp::BaseComponent
61
- ```
62
-
63
55
  ### Verification System
64
56
 
65
- 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.json`.
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`.
66
58
 
67
59
  ## Implementation Notes
68
60
 
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,24 +37,25 @@ 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)
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
44
+ ### Components Directory
35
45
 
36
- By default, the cops detect classes that inherit from `ViewComponent::Base` or `ApplicationComponent`. If your project uses a different base class (e.g. `Primer::Component`), you can configure additional parent classes under `AllCops`, for example:
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`:
37
47
 
38
48
  ```yaml
39
49
  # .rubocop.yml
40
- AllCops:
41
- ViewComponentParentClasses:
42
- - MyApp::BaseComponent
50
+ ViewComponent/ComponentSuffix:
51
+ Include:
52
+ - 'app/components/**/*.rb'
53
+ - 'engines/*/app/components/**/*.rb'
43
54
  ```
44
55
 
45
56
  ### No Super
46
57
 
47
- View Component convention is to not calling `super` in component initializers, but that may cause `Lint/MissingSuper` failures from RuboCop. We suggest disabling that rule for your view components directory, for example:
58
+ ViewComponent convention is to not call `super` in component initializers, but that may cause `Lint/MissingSuper` failures from RuboCop. We suggest disabling that rule for your view components directory, for example:
48
59
 
49
60
  ```yaml
50
61
  # .rubocop.yml
data/Rakefile CHANGED
@@ -1,13 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- require "minitest/test_task"
4
+ require "rubocop/rake_task"
5
5
 
6
- Minitest::TestTask.create
6
+ RuboCop::RakeTask.new
7
7
 
8
- require "standard/rake"
9
-
10
- task default: %i[spec standard]
8
+ task default: %i[spec rubocop]
11
9
 
12
10
  require "rspec/core/rake_task"
13
11
 
data/config/default.yml CHANGED
@@ -1,5 +1,4 @@
1
1
  ViewComponent:
2
- ViewComponentParentClasses: []
3
2
  ComponentNamespaces: []
4
3
 
5
4
  ViewComponent/ComponentSuffix:
@@ -8,6 +7,8 @@ ViewComponent/ComponentSuffix:
8
7
  VersionAdded: '0.1'
9
8
  Severity: convention
10
9
  StyleGuide: 'https://viewcomponent.org/best_practices.html'
10
+ Include:
11
+ - 'app/components/**/*.rb'
11
12
 
12
13
  ViewComponent/MissingPreview:
13
14
  Description: 'Ensure every ViewComponent has a corresponding preview file.'
@@ -52,6 +53,8 @@ ViewComponent/PreferComposition:
52
53
  VersionAdded: '0.3'
53
54
  Severity: convention
54
55
  StyleGuide: 'https://viewcomponent.org/best_practices.html#avoid-inheritance'
56
+ Include:
57
+ - 'app/components/**/*.rb'
55
58
  ComponentNamespaces: []
56
59
 
57
60
  ViewComponent/PreferSlots:
@@ -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,33 +1,64 @@
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
- class_source = node.identifier.source
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 ViewComponent::Base, ApplicationComponent,
22
- # or a configured additional parent class
30
+ # Check if a constant node resolves to a class with ViewComponent::Base as ancestor
23
31
  def view_component_parent?(node)
24
32
  return false unless node.const_type?
25
33
 
26
- source = node.source
27
- return true if source == "ViewComponent::Base" || source == "ApplicationComponent"
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)
40
+ end
41
+
42
+ # Check if a class node is itself an abstract parent class
43
+ # (has ViewComponent::Base as ancestor and has descendants in the project)
44
+ def view_component_parent_class?(node)
45
+ return false unless node&.class_type?
46
+
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 }
54
+ end
55
+
56
+ def fully_qualified_name(node)
57
+ namespace = node.parent_module_name
58
+ short_name = node.identifier.source
59
+ return short_name if namespace.nil? || namespace == "Object"
28
60
 
29
- additional = cop_config["ViewComponentParentClasses"] || []
30
- additional.include?(source)
61
+ "#{namespace}::#{short_name}"
31
62
  end
32
63
 
33
64
  def component_namespaces
@@ -41,11 +72,22 @@ module RuboCop
41
72
 
42
73
  # Check if node is within a ViewComponent class
43
74
  def inside_view_component?(node)
75
+ require_project_index!
76
+
44
77
  klass = enclosing_class(node)
45
78
  return false unless klass
46
79
 
47
80
  view_component_class?(klass)
48
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
49
91
  end
50
92
  end
51
93
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/inflector"
4
+
3
5
  module RuboCop
4
6
  module Cop
5
7
  module ViewComponent
@@ -15,8 +17,9 @@ module RuboCop
15
17
 
16
18
  def on_class(node)
17
19
  return unless view_component_class?(node)
20
+ return if view_component_parent_class?(node)
18
21
 
19
- class_name = node.identifier.source
22
+ class_name = fully_qualified_name(node)
20
23
  return if preview_exists?(class_name)
21
24
 
22
25
  add_offense(node.identifier, message: format(MSG, component: class_name, paths: preview_paths.join(", ")))
@@ -33,12 +36,11 @@ module RuboCop
33
36
  end
34
37
 
35
38
  def candidate_filenames(class_name)
36
- base = class_name.gsub(/Component$/, "").gsub("::", "/").gsub(/([A-Z])/, '_\1').downcase.gsub("/_", "/")
37
- base = base.delete_prefix("_").delete_prefix("/")
38
- [
39
- "#{base}_preview.rb",
40
- "#{base}_component_preview.rb"
41
- ]
39
+ bases = [ActiveSupport::Inflector.underscore(class_name.delete_suffix("Component"))]
40
+ short_name = class_name.split("::").last
41
+ short_base = ActiveSupport::Inflector.underscore(short_name.delete_suffix("Component"))
42
+ bases << short_base if short_base != bases.first
43
+ bases.flat_map { |base| ["#{base}_preview.rb", "#{base}_component_preview.rb"] }
42
44
  end
43
45
 
44
46
  def preview_paths
@@ -45,18 +45,32 @@ module RuboCop
45
45
 
46
46
  RESTRICT_ON_SEND = GLOBAL_STATE_METHODS
47
47
 
48
+ # @!method global_state_access?(node)
48
49
  def_node_matcher :global_state_access?, <<~PATTERN
49
50
  (send nil? ${:params :request :session :cookies :flash} ...)
50
51
  PATTERN
51
52
 
53
+ # @!method sorbet_sig_block?(node)
54
+ def_node_matcher :sorbet_sig_block?, <<~PATTERN
55
+ (block (send nil? :sig) ...)
56
+ PATTERN
57
+
52
58
  def on_send(node)
53
59
  return unless inside_view_component?(node)
54
60
 
55
61
  method_name = global_state_access?(node)
56
62
  return unless method_name
63
+ return if sorbet_signature?(node)
57
64
 
58
65
  add_offense(node, message: format(MSG, method: method_name))
59
66
  end
67
+ alias on_csend on_send
68
+
69
+ private
70
+
71
+ def sorbet_signature?(node)
72
+ node.each_ancestor(:block).any? { |ancestor| sorbet_sig_block?(ancestor) }
73
+ end
60
74
  end
61
75
  end
62
76
  end
@@ -40,29 +40,32 @@ module RuboCop
40
40
  private
41
41
 
42
42
  def check_public_methods(class_node)
43
- current_visibility = :public
44
- template_method_calls = methods_called_in_templates
45
-
46
43
  body = class_node.body
47
44
  return unless body
48
45
 
49
46
  children = body.begin_type? ? body.children : [body]
47
+ check_children(children, methods_called_in_templates)
48
+ end
49
+
50
+ def check_children(children, template_method_calls)
51
+ current_visibility = :public
50
52
 
51
53
  children.each do |child|
52
54
  if visibility_modifier?(child)
53
55
  current_visibility = child.method_name
54
- next
56
+ elsif should_flag?(child, current_visibility, template_method_calls)
57
+ add_offense(child, message: format(MSG, method_name: child.method_name))
55
58
  end
56
-
57
- next unless child.def_type?
58
- next unless current_visibility == :public
59
- next if allowed_public_method?(child.method_name)
60
- next if template_method_calls.include?(child.method_name)
61
-
62
- add_offense(child, message: format(MSG, method_name: child.method_name))
63
59
  end
64
60
  end
65
61
 
62
+ def should_flag?(child, visibility, template_method_calls)
63
+ child.def_type? &&
64
+ visibility == :public &&
65
+ !allowed_public_method?(child.method_name) &&
66
+ !template_method_calls.include?(child.method_name)
67
+ end
68
+
66
69
  def allowed_public_method?(method_name)
67
70
  allowed_public_methods.include?(method_name.to_s) ||
68
71
  allowed_public_method_patterns.any? { |pattern| method_name.to_s.match?(pattern) }
@@ -84,8 +87,7 @@ module RuboCop
84
87
  template_paths.each_with_object(Set.new) do |path, methods|
85
88
  methods.merge(extract_method_calls(path))
86
89
  end
87
- rescue => e
88
- # Graceful degradation on errors
90
+ rescue StandardError => e
89
91
  warn "Warning: Failed to analyze templates: #{e.message}" if ENV["RUBOCOP_DEBUG"]
90
92
  Set.new
91
93
  end
@@ -31,6 +31,7 @@ module RuboCop
31
31
 
32
32
  HTML_PARAM_PATTERN = /_html$/
33
33
 
34
+ # @!method html_safe_call?(node)
34
35
  def_node_search :html_safe_call?, "(send _ :html_safe)"
35
36
 
36
37
  def on_class(node)
@@ -46,29 +47,23 @@ module RuboCop
46
47
 
47
48
  def find_initialize(class_node)
48
49
  class_node.each_descendant(:def).find do |def_node|
49
- def_node.method_name == :initialize
50
+ def_node.method?(:initialize)
50
51
  end
51
52
  end
52
53
 
53
54
  def check_initialize_params(initialize_node)
54
55
  initialize_node.arguments.each do |arg|
55
- next unless arg.kwoptarg_type? || arg.kwarg_type?
56
+ next unless arg.type?(:kwoptarg, :kwarg)
56
57
 
57
- param_name = arg.children[0]
58
+ check_param(arg)
59
+ end
60
+ end
58
61
 
59
- # Check parameter name patterns
60
- if html_param_name?(param_name)
61
- suggested_slot = suggest_slot_name(param_name)
62
- add_offense(arg, message: format(MSG, slot_method: suggested_slot))
63
- next
64
- end
62
+ def check_param(arg)
63
+ param_name = arg.children[0]
64
+ return unless html_param_name?(param_name) || (arg.kwoptarg_type? && html_safe_call?(arg))
65
65
 
66
- # Check for html_safe in default value
67
- if arg.kwoptarg_type? && html_safe_call?(arg)
68
- suggested_slot = suggest_slot_name(param_name)
69
- add_offense(arg, message: format(MSG, slot_method: suggested_slot))
70
- end
71
- end
66
+ add_offense(arg, message: format(MSG, slot_method: suggest_slot_name(param_name)))
72
67
  end
73
68
 
74
69
  def html_param_name?(name)