rubocop-view_component 0.2.0 → 0.3.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/CLAUDE.md +72 -0
- data/README.md +71 -23
- data/config/default.yml +17 -0
- data/lib/rubocop/cop/view_component/prefer_composition.rb +44 -0
- data/lib/rubocop/cop/view_component/prefer_private_methods.rb +2 -2
- data/lib/rubocop/cop/view_component/prefer_slots.rb +3 -23
- data/lib/rubocop/cop/view_component/test_rendered_output.rb +72 -0
- data/lib/rubocop/cop/view_component_cops.rb +2 -0
- data/lib/rubocop/view_component/version.rb +1 -1
- data/script/verify +178 -0
- data/spec/expected_govuk_failures.json +200 -0
- data/spec/expected_polaris_failures.json +356 -0
- data/spec/expected_primer_failures.json +47 -35
- data/spec/rubocop/cop/view_component/prefer_composition_spec.rb +83 -0
- data/spec/rubocop/cop/view_component/prefer_private_methods_spec.rb +8 -8
- data/spec/rubocop/cop/view_component/prefer_slots_spec.rb +24 -38
- data/spec/rubocop/cop/view_component/test_rendered_output_spec.rb +134 -0
- data/verification/govuk_rubocop_config.yml +3 -0
- data/verification/libraries.yml +11 -0
- data/verification/polaris_rubocop_config.yml +4 -0
- data/verification/primer_rubocop_config.yml +19 -0
- metadata +13 -5
- data/IMPLEMENTATION_PLAN.md +0 -177
- data/IMPLEMENTATION_SUMMARY.md +0 -172
- data/PLAN.md +0 -625
- data/script/verify_against_primer.rb +0 -128
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 60cf1f19ad3af3d8c80a7da526c8fcc77658723067e67bb42b6ce4bd97709302
|
|
4
|
+
data.tar.gz: ac9a362b04a16a58549e0733c72fbf874ae7ca954ce0509eac5fe763f00283c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7282cc87157b8b189a735b776cf93d48f9084b93a60a637e8a349806cef1706d1aa3581a308a2ccac21cdff7fe7613dd6fd0098c5d542493f2a98c87b8280bea
|
|
7
|
+
data.tar.gz: bc67d21c75e60619c11e75db291f602f8a9967a7b1c31041341207a4ccba72e9aafe3029fd1efdb9db2a60c36b68e4b961e7fe40c50bd50d8023958ea0472944
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
This is a RuboCop extension that enforces ViewComponent best practices. It provides custom cops that detect anti-patterns and style issues specific to ViewComponent development.
|
|
8
|
+
|
|
9
|
+
## Common Commands
|
|
10
|
+
|
|
11
|
+
### Testing
|
|
12
|
+
- `rake spec` - Run all RSpec tests
|
|
13
|
+
- `bundle exec rspec spec/rubocop/cop/view_component/FILENAME_spec.rb` - Run a specific spec file
|
|
14
|
+
- `rake standard` - Run Standard (RuboCop) linting
|
|
15
|
+
- `rake` - Run both tests and linting (default task)
|
|
16
|
+
|
|
17
|
+
### Verification
|
|
18
|
+
The project includes a verification system that tests cops against real-world component libraries:
|
|
19
|
+
|
|
20
|
+
- `script/verify primer` - Verify against Primer ViewComponents
|
|
21
|
+
- `script/verify govuk` - Verify against x-govuk components
|
|
22
|
+
- `script/verify polaris` - Verify against Polaris ViewComponents
|
|
23
|
+
- `script/verify LIBRARY --regenerate` - Update expected results after intentional changes
|
|
24
|
+
- `script/verify LIBRARY --update` - Force re-download latest library source
|
|
25
|
+
|
|
26
|
+
### Development
|
|
27
|
+
- `bundle exec rake new_cop[ViewComponent/CopName]` - Generate a new cop with template files
|
|
28
|
+
|
|
29
|
+
## Code Architecture
|
|
30
|
+
|
|
31
|
+
### Cop Structure
|
|
32
|
+
|
|
33
|
+
All cops inherit from `RuboCop::Cop::Base` and are located in `lib/rubocop/cop/view_component/`. Each cop:
|
|
34
|
+
|
|
35
|
+
1. Includes `ViewComponent::Base` module for shared helper methods
|
|
36
|
+
2. Defines detection logic in `on_class`, `on_def`, or other AST node callbacks
|
|
37
|
+
3. Has configuration in `config/default.yml`
|
|
38
|
+
4. Has corresponding specs in `spec/rubocop/cop/view_component/`
|
|
39
|
+
|
|
40
|
+
### Shared Modules
|
|
41
|
+
|
|
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
|
|
45
|
+
- Provides `inside_view_component?(node)` - Checks if code is within a ViewComponent
|
|
46
|
+
|
|
47
|
+
**`TemplateAnalyzer`** (`lib/rubocop/cop/view_component/template_analyzer.rb`)
|
|
48
|
+
- Used by PreferPrivateMethods cop to analyze ERB templates
|
|
49
|
+
- Extracts method calls from templates to avoid flagging template-used methods as private candidates
|
|
50
|
+
- Handles both sibling templates (`component.html.erb`) and sidecar templates (`component/component.html.erb`)
|
|
51
|
+
- Uses the `herb` gem to parse ERB and extract Ruby code
|
|
52
|
+
|
|
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
|
+
### Verification System
|
|
64
|
+
|
|
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`.
|
|
66
|
+
|
|
67
|
+
## Implementation Notes
|
|
68
|
+
|
|
69
|
+
- When adding a new cop, use `rake new_cop[ViewComponent/CopName]` to generate the boilerplate
|
|
70
|
+
- Template analysis is performance-sensitive - `PreferPrivateMethods` uses `herb` gem for efficient ERB parsing
|
|
71
|
+
- Cops must handle graceful degradation when templates can't be parsed
|
|
72
|
+
- All cops should include `ViewComponent::Base` module to get detection helpers
|
data/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# rubocop-view_component
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
A RuboCop extension that enforces [ViewComponent best practices](https://viewcomponent.org/best_practices.html).
|
|
3
|
+
A RuboCop extension that encourages [ViewComponent best practices](https://viewcomponent.org/best_practices.html).
|
|
6
4
|
|
|
7
5
|
## Installation
|
|
8
6
|
|
|
@@ -27,32 +25,32 @@ This gem provides several cops to enforce ViewComponent best practices:
|
|
|
27
25
|
- **ViewComponent/NoGlobalState** - Prevent direct access to `params`, `request`, `session`, etc.
|
|
28
26
|
- **ViewComponent/PreferPrivateMethods** - Suggest making helper methods private (analyzes ERB templates to avoid flagging methods used in views)
|
|
29
27
|
- **ViewComponent/PreferSlots** - Detect HTML parameters that should be slots
|
|
30
|
-
- **ViewComponent/PreferComposition** -
|
|
28
|
+
- **ViewComponent/PreferComposition** - Avoid inheriting one ViewComponent from another (prefer composition)
|
|
31
29
|
- **ViewComponent/TestRenderedOutput** - Encourage testing rendered output over private methods
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
## Usage
|
|
36
|
-
|
|
37
|
-
Run RuboCop as usual:
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
bundle exec rubocop
|
|
41
|
-
```
|
|
31
|
+
## Optional Configuration
|
|
42
32
|
|
|
43
|
-
|
|
33
|
+
### Base Class
|
|
44
34
|
|
|
45
|
-
By default,
|
|
35
|
+
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
36
|
|
|
47
37
|
```yaml
|
|
48
38
|
# .rubocop.yml
|
|
49
39
|
AllCops:
|
|
50
40
|
ViewComponentParentClasses:
|
|
51
|
-
- Primer::Component
|
|
52
41
|
- MyApp::BaseComponent
|
|
53
42
|
```
|
|
54
43
|
|
|
55
|
-
|
|
44
|
+
### No Super
|
|
45
|
+
|
|
46
|
+
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:
|
|
47
|
+
|
|
48
|
+
```yaml
|
|
49
|
+
# .rubocop.yml
|
|
50
|
+
Lint/MissingSuper:
|
|
51
|
+
Exclude:
|
|
52
|
+
- 'app/components/**/*'
|
|
53
|
+
```
|
|
56
54
|
|
|
57
55
|
## Development
|
|
58
56
|
|
|
@@ -60,20 +58,70 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
|
60
58
|
|
|
61
59
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
62
60
|
|
|
63
|
-
##
|
|
61
|
+
## Real-World Verification
|
|
62
|
+
|
|
63
|
+
The cops are tested against real-world component libraries as baselines to catch regressions.
|
|
64
64
|
|
|
65
|
-
The
|
|
65
|
+
The [`script/verify`](script/verify) script downloads component libraries (cached in `verification/`), runs all ViewComponent cops against them, and compares the results to checked-in snapshots. This runs automatically in CI.
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
### Primer ViewComponents
|
|
68
|
+
|
|
69
|
+
To verify against [primer/view_components](https://github.com/primer/view_components) locally:
|
|
68
70
|
|
|
69
71
|
```bash
|
|
70
|
-
|
|
72
|
+
script/verify primer
|
|
71
73
|
```
|
|
72
74
|
|
|
73
75
|
If you intentionally change cop behavior, regenerate the snapshot:
|
|
74
76
|
|
|
75
77
|
```bash
|
|
76
|
-
|
|
78
|
+
script/verify primer --regenerate
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
To force download the latest Primer source:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
script/verify primer --update
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### x-govuk Components
|
|
88
|
+
|
|
89
|
+
To verify against [x-govuk/govuk-components](https://github.com/x-govuk/govuk-components) locally:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
script/verify govuk
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If you intentionally change cop behavior, regenerate the snapshot:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
script/verify govuk --regenerate
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
To force download the latest x-govuk source:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
script/verify govuk --update
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Polaris ViewComponents
|
|
108
|
+
|
|
109
|
+
To verify against [baoagency/polaris_view_components](https://github.com/baoagency/polaris_view_components) locally:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
script/verify polaris
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
If you intentionally change cop behavior, regenerate the snapshot:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
script/verify polaris --regenerate
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
To force download the latest Polaris source:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
script/verify polaris --update
|
|
77
125
|
```
|
|
78
126
|
|
|
79
127
|
## Contributing
|
data/config/default.yml
CHANGED
|
@@ -32,9 +32,26 @@ ViewComponent/PreferPrivateMethods:
|
|
|
32
32
|
AllowedPublicMethodPatterns:
|
|
33
33
|
- "^with_"
|
|
34
34
|
|
|
35
|
+
ViewComponent/PreferComposition:
|
|
36
|
+
Description: 'Prefer composition over inheritance for ViewComponents.'
|
|
37
|
+
Enabled: true
|
|
38
|
+
VersionAdded: '0.3'
|
|
39
|
+
Severity: convention
|
|
40
|
+
StyleGuide: 'https://viewcomponent.org/best_practices.html'
|
|
41
|
+
|
|
35
42
|
ViewComponent/PreferSlots:
|
|
36
43
|
Description: 'Prefer slots over HTML string parameters.'
|
|
37
44
|
Enabled: true
|
|
38
45
|
VersionAdded: '0.1'
|
|
39
46
|
Severity: convention
|
|
40
47
|
StyleGuide: 'https://viewcomponent.org/best_practices.html'
|
|
48
|
+
|
|
49
|
+
ViewComponent/TestRenderedOutput:
|
|
50
|
+
Description: 'Test rendered output instead of component instance methods.'
|
|
51
|
+
Enabled: true
|
|
52
|
+
VersionAdded: '0.3'
|
|
53
|
+
Severity: convention
|
|
54
|
+
StyleGuide: 'https://viewcomponent.org/guide/testing.html'
|
|
55
|
+
Include:
|
|
56
|
+
- 'spec/components/**/*_spec.rb'
|
|
57
|
+
- 'test/components/**/*_test.rb'
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module ViewComponent
|
|
6
|
+
# Detects ViewComponent classes that inherit from another component
|
|
7
|
+
# instead of using composition. Inheriting one component from another
|
|
8
|
+
# causes confusion when each has its own template.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# # bad
|
|
12
|
+
# class UserCardComponent < BaseCardComponent
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# # good
|
|
16
|
+
# class UserCardComponent < ViewComponent::Base
|
|
17
|
+
# # Render BaseCardComponent within template via composition
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
class PreferComposition < RuboCop::Cop::Base
|
|
21
|
+
include ViewComponent::Base
|
|
22
|
+
|
|
23
|
+
MSG = "Avoid inheriting from another ViewComponent."
|
|
24
|
+
|
|
25
|
+
def on_class(node)
|
|
26
|
+
parent_class = node.parent_class
|
|
27
|
+
return unless parent_class
|
|
28
|
+
return if view_component_parent?(parent_class)
|
|
29
|
+
return unless component_like_parent?(parent_class)
|
|
30
|
+
|
|
31
|
+
add_offense(parent_class)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def component_like_parent?(node)
|
|
37
|
+
return false unless node.const_type?
|
|
38
|
+
|
|
39
|
+
node.source.end_with?("Component")
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -28,7 +28,7 @@ module RuboCop
|
|
|
28
28
|
include ViewComponent::Base
|
|
29
29
|
include TemplateAnalyzer
|
|
30
30
|
|
|
31
|
-
MSG = "Consider making
|
|
31
|
+
MSG = "Consider making `%<method_name>s` private. " \
|
|
32
32
|
"Only ViewComponent interface methods should be public."
|
|
33
33
|
|
|
34
34
|
def on_class(node)
|
|
@@ -59,7 +59,7 @@ module RuboCop
|
|
|
59
59
|
next if allowed_public_method?(child.method_name)
|
|
60
60
|
next if template_method_calls.include?(child.method_name)
|
|
61
61
|
|
|
62
|
-
add_offense(child)
|
|
62
|
+
add_offense(child, message: format(MSG, method_name: child.method_name))
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
|
|
@@ -29,20 +29,7 @@ module RuboCop
|
|
|
29
29
|
MSG = "Consider using `%<slot_method>s` instead of passing HTML " \
|
|
30
30
|
"as a parameter. This maintains Rails' automatic HTML escaping."
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
/_html$/,
|
|
34
|
-
/_content$/,
|
|
35
|
-
/^html_/,
|
|
36
|
-
/^content$/
|
|
37
|
-
].freeze
|
|
38
|
-
|
|
39
|
-
# Exclude common non-HTML parameters
|
|
40
|
-
EXCLUDED_PARAMS = %i[
|
|
41
|
-
html_class
|
|
42
|
-
html_classes
|
|
43
|
-
html_id
|
|
44
|
-
html_tag
|
|
45
|
-
].freeze
|
|
32
|
+
HTML_PARAM_PATTERN = /_html$/
|
|
46
33
|
|
|
47
34
|
def_node_search :html_safe_call?, "(send _ :html_safe)"
|
|
48
35
|
|
|
@@ -69,9 +56,6 @@ module RuboCop
|
|
|
69
56
|
|
|
70
57
|
param_name = arg.children[0]
|
|
71
58
|
|
|
72
|
-
# Skip excluded parameters
|
|
73
|
-
next if EXCLUDED_PARAMS.include?(param_name)
|
|
74
|
-
|
|
75
59
|
# Check parameter name patterns
|
|
76
60
|
if html_param_name?(param_name)
|
|
77
61
|
suggested_slot = suggest_slot_name(param_name)
|
|
@@ -88,15 +72,11 @@ module RuboCop
|
|
|
88
72
|
end
|
|
89
73
|
|
|
90
74
|
def html_param_name?(name)
|
|
91
|
-
|
|
75
|
+
HTML_PARAM_PATTERN.match?(name.to_s)
|
|
92
76
|
end
|
|
93
77
|
|
|
94
78
|
def suggest_slot_name(param_name)
|
|
95
|
-
clean_name = param_name.to_s
|
|
96
|
-
.sub(/_html$/, "")
|
|
97
|
-
.sub(/_content$/, "")
|
|
98
|
-
.sub(/^html_/, "")
|
|
99
|
-
|
|
79
|
+
clean_name = param_name.to_s.sub(/_html$/, "")
|
|
100
80
|
"renders_one :#{clean_name}"
|
|
101
81
|
end
|
|
102
82
|
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module ViewComponent
|
|
6
|
+
# Ensures that ViewComponent tests use `render_inline` to test rendered output
|
|
7
|
+
# rather than testing component methods directly.
|
|
8
|
+
#
|
|
9
|
+
# This cop is only enabled for test files by default (see config).
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad
|
|
13
|
+
# def test_formatted_title
|
|
14
|
+
# component = TitleComponent.new("hello")
|
|
15
|
+
# assert_equal "HELLO", component.formatted_title
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# # good
|
|
19
|
+
# def test_formatted_title
|
|
20
|
+
# render_inline TitleComponent.new("hello")
|
|
21
|
+
# assert_text "HELLO"
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
class TestRenderedOutput < RuboCop::Cop::Base
|
|
25
|
+
MSG = "Test instantiates a component but doesn't use `render_inline` or `render_preview`. " \
|
|
26
|
+
"Test the rendered output instead of component methods directly."
|
|
27
|
+
|
|
28
|
+
# Check Minitest-style test methods
|
|
29
|
+
def on_def(node)
|
|
30
|
+
method_name = node.method_name.to_s
|
|
31
|
+
return unless method_name.start_with?("test_")
|
|
32
|
+
return unless instantiates_component?(node)
|
|
33
|
+
return if contains_render_method?(node)
|
|
34
|
+
|
|
35
|
+
add_offense(node)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Check RSpec-style it blocks
|
|
39
|
+
def on_block(node)
|
|
40
|
+
return unless rspec_it_block?(node)
|
|
41
|
+
return unless instantiates_component?(node)
|
|
42
|
+
return if contains_render_method?(node)
|
|
43
|
+
|
|
44
|
+
add_offense(node)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def instantiates_component?(node)
|
|
50
|
+
node.each_descendant(:send).any? do |send_node|
|
|
51
|
+
next unless send_node.method_name == :new
|
|
52
|
+
|
|
53
|
+
send_node.receiver&.const_type?
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def contains_render_method?(node)
|
|
58
|
+
node.each_descendant(:send).any? do |send_node|
|
|
59
|
+
%i[render_inline render_preview].include?(send_node.method_name)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def rspec_it_block?(node)
|
|
64
|
+
send_node = node.send_node
|
|
65
|
+
return false unless send_node
|
|
66
|
+
|
|
67
|
+
%i[it specify example].include?(send_node.method_name)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -4,4 +4,6 @@ require_relative "view_component/base"
|
|
|
4
4
|
require_relative "view_component/component_suffix"
|
|
5
5
|
require_relative "view_component/no_global_state"
|
|
6
6
|
require_relative "view_component/prefer_private_methods"
|
|
7
|
+
require_relative "view_component/prefer_composition"
|
|
7
8
|
require_relative "view_component/prefer_slots"
|
|
9
|
+
require_relative "view_component/test_rendered_output"
|
data/script/verify
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "yaml"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
require "open3"
|
|
9
|
+
require "bundler"
|
|
10
|
+
|
|
11
|
+
GEM_DIR = File.expand_path("..", __dir__)
|
|
12
|
+
LIBRARIES_CONFIG = File.join(GEM_DIR, "verification", "libraries.yml")
|
|
13
|
+
|
|
14
|
+
def load_libraries
|
|
15
|
+
config = YAML.load_file(LIBRARIES_CONFIG)
|
|
16
|
+
|
|
17
|
+
# Build full paths for each library
|
|
18
|
+
config.transform_values do |library|
|
|
19
|
+
library_key = config.key(library)
|
|
20
|
+
{
|
|
21
|
+
tarball_url: library["tarball_url"],
|
|
22
|
+
verification_dir: File.join(GEM_DIR, "verification", library_key),
|
|
23
|
+
config_file: File.join(GEM_DIR, "verification", "#{library_key}_rubocop_config.yml"),
|
|
24
|
+
results_file: File.join(GEM_DIR, "spec", "expected_#{library_key}_failures.json"),
|
|
25
|
+
display_name: library["display_name"]
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def main
|
|
31
|
+
libraries = load_libraries
|
|
32
|
+
library = parse_library_arg(libraries)
|
|
33
|
+
|
|
34
|
+
config = libraries[library]
|
|
35
|
+
mode = ARGV.include?("--regenerate") ? :regenerate : :verify
|
|
36
|
+
force_update = ARGV.include?("--update")
|
|
37
|
+
|
|
38
|
+
if force_update && Dir.exist?(config[:verification_dir])
|
|
39
|
+
puts "Removing existing #{config[:display_name]} source for update..."
|
|
40
|
+
FileUtils.rm_rf(config[:verification_dir])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if Dir.exist?(config[:verification_dir]) && !Dir.empty?(config[:verification_dir])
|
|
44
|
+
puts "Using existing #{config[:display_name]} source at #{config[:verification_dir]}"
|
|
45
|
+
else
|
|
46
|
+
FileUtils.mkdir_p(config[:verification_dir])
|
|
47
|
+
download_source(config[:verification_dir], config[:tarball_url], config[:display_name])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
Dir.chdir(config[:verification_dir]) do
|
|
51
|
+
configure_rubocop(config[:config_file], config[:display_name])
|
|
52
|
+
add_gem_to_gemfile
|
|
53
|
+
|
|
54
|
+
Bundler.with_unbundled_env do
|
|
55
|
+
output = run_rubocop
|
|
56
|
+
offenses = extract_offenses(output)
|
|
57
|
+
|
|
58
|
+
case mode
|
|
59
|
+
when :regenerate then regenerate(offenses, config[:results_file])
|
|
60
|
+
when :verify then verify(offenses, config[:results_file])
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def parse_library_arg(libraries)
|
|
67
|
+
# Find the library argument (not --flags)
|
|
68
|
+
library_arg = ARGV.find { |arg| !arg.start_with?("--") }
|
|
69
|
+
|
|
70
|
+
if library_arg.nil?
|
|
71
|
+
abort "Usage: #{$PROGRAM_NAME} <library> [--regenerate] [--update]\n" \
|
|
72
|
+
" library: #{libraries.keys.join(", ")}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
unless libraries.key?(library_arg)
|
|
76
|
+
abort "ERROR: Unknown library '#{library_arg}'. Valid options: #{libraries.keys.join(", ")}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
library_arg
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def system!(*args)
|
|
83
|
+
system(*args, exception: true)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def download_source(dir, tarball_url, display_name)
|
|
87
|
+
puts "Downloading #{display_name}..."
|
|
88
|
+
system!("curl", "-sL", tarball_url, "-o", "#{dir}/source.tar.gz")
|
|
89
|
+
system!("tar", "xz", "-C", dir, "--strip-components=1", "-f", "#{dir}/source.tar.gz")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
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
|
|
105
|
+
|
|
106
|
+
File.write(".rubocop.yml", YAML.dump(config))
|
|
107
|
+
end
|
|
108
|
+
|
|
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}'" }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def run_rubocop
|
|
115
|
+
puts "Running bundle install..."
|
|
116
|
+
system!("bundle", "install")
|
|
117
|
+
|
|
118
|
+
puts "Running RuboCop (ViewComponent cops only)..."
|
|
119
|
+
output, status = Open3.capture2(
|
|
120
|
+
"bundle", "exec", "rubocop",
|
|
121
|
+
"--require", "rubocop-view_component",
|
|
122
|
+
"--only", "ViewComponent",
|
|
123
|
+
"--format", "json"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
puts "RuboCop exit status: #{status.exitstatus}"
|
|
127
|
+
|
|
128
|
+
if output.strip.empty?
|
|
129
|
+
abort "ERROR: RuboCop produced no output (exit status: #{status.exitstatus})"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
output
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def extract_offenses(rubocop_output)
|
|
136
|
+
data = JSON.parse(rubocop_output)
|
|
137
|
+
data["files"].flat_map do |file|
|
|
138
|
+
file["offenses"].map do |offense|
|
|
139
|
+
{
|
|
140
|
+
"path" => file["path"],
|
|
141
|
+
"line" => offense["location"]["start_line"],
|
|
142
|
+
"cop" => offense["cop_name"],
|
|
143
|
+
"message" => offense["message"]
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def regenerate(offenses, results_file)
|
|
150
|
+
json = "#{JSON.pretty_generate(offenses)}\n"
|
|
151
|
+
File.write(results_file, json)
|
|
152
|
+
puts "#{offenses.length} offense(s) written to #{results_file}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def verify(offenses, results_file)
|
|
156
|
+
unless File.exist?(results_file)
|
|
157
|
+
abort "ERROR: #{results_file} not found. Run '#{$PROGRAM_NAME} --regenerate' first."
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
current_json = JSON.pretty_generate(offenses)
|
|
161
|
+
expected_json = File.read(results_file)
|
|
162
|
+
|
|
163
|
+
if current_json.strip == expected_json.strip
|
|
164
|
+
puts "Verification passed: output matches #{results_file}"
|
|
165
|
+
else
|
|
166
|
+
puts "Verification failed: output differs from #{results_file}"
|
|
167
|
+
expected = JSON.parse(expected_json)
|
|
168
|
+
added = offenses - expected
|
|
169
|
+
removed = expected - offenses
|
|
170
|
+
|
|
171
|
+
added.each { |o| puts " + #{o["cop"]}: #{o["path"]}:#{o["line"]}" }
|
|
172
|
+
removed.each { |o| puts " - #{o["cop"]}: #{o["path"]}:#{o["line"]}" }
|
|
173
|
+
|
|
174
|
+
exit 1
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
main
|