rubocop-view_component 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +33 -0
- data/Appraisals +5 -0
- data/CLAUDE.md +6 -14
- data/README.md +18 -7
- data/Rakefile +3 -5
- data/config/default.yml +4 -1
- data/gemfiles/rubocop_minimum.gemfile +13 -0
- data/gemfiles/rubocop_minimum.gemfile.lock +162 -0
- data/lib/rubocop/cop/view_component/base.rb +51 -9
- data/lib/rubocop/cop/view_component/missing_preview.rb +9 -7
- data/lib/rubocop/cop/view_component/no_global_state.rb +14 -0
- data/lib/rubocop/cop/view_component/prefer_private_methods.rb +15 -13
- data/lib/rubocop/cop/view_component/prefer_slots.rb +10 -15
- data/lib/rubocop/cop/view_component/template_analyzer.rb +19 -19
- data/lib/rubocop/cop/view_component/test_rendered_output.rb +4 -1
- data/lib/rubocop/view_component/version.rb +1 -1
- data/script/verify +16 -17
- data/spec/expected_govuk_failures.yml +2 -0
- data/spec/expected_polaris_failures.yml +0 -1
- data/spec/expected_primer_failures.yml +3 -0
- data/spec/rubocop/cop/view_component/base_spec.rb +92 -0
- data/spec/rubocop/cop/view_component/component_suffix_spec.rb +0 -33
- data/spec/rubocop/cop/view_component/missing_preview_spec.rb +83 -5
- data/spec/rubocop/cop/view_component/no_global_state_spec.rb +13 -0
- data/spec/rubocop/cop/view_component/prefer_composition_spec.rb +0 -17
- data/spec/rubocop/cop/view_component/prefer_private_methods_spec.rb +3 -3
- data/spec/rubocop/cop/view_component/test_rendered_output_spec.rb +1 -1
- data/spec/spec_helper.rb +10 -0
- data/spec/support/project_index_helpers.rb +71 -0
- data/verification/govuk_rubocop_config.yml +3 -3
- data/verification/polaris_rubocop_config.yml +3 -4
- data/verification/primer_rubocop_config.yml +5 -4
- metadata +39 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fa0434208243f306c17f51b681abdf59f6152d6afdd0014c608c5b4e08e893c2
|
|
4
|
+
data.tar.gz: e709241be864e024f8ce410b6620d62bcdd12cd090e2593b378c0169a628f1e9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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
|
|
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
|
-
-
|
|
44
|
-
- Provides `
|
|
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.
|
|
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
|
-
###
|
|
44
|
+
### Components Directory
|
|
35
45
|
|
|
36
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
-
|
|
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
|
-
|
|
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 "
|
|
4
|
+
require "rubocop/rake_task"
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
RuboCop::RakeTask.new
|
|
7
7
|
|
|
8
|
-
|
|
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,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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
56
|
+
next unless arg.type?(:kwoptarg, :kwarg)
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
check_param(arg)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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)
|