rubocop-view_component 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/{CLAUDE.md → AGENTS.md} +6 -14
  4. data/Appraisals +5 -0
  5. data/README.md +50 -27
  6. data/config/default.yml +6 -5
  7. data/docs/rubydex.md +136 -0
  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 +56 -7
  11. data/lib/rubocop/cop/view_component/prefer_composition.rb +42 -9
  12. data/lib/rubocop/cop/view_component/prefer_private_methods.rb +46 -13
  13. data/lib/rubocop/cop/view_component/rspec_scope.rb +98 -0
  14. data/lib/rubocop/cop/view_component/supplementary_index.rb +142 -0
  15. data/lib/rubocop/cop/view_component/test_rendered_output.rb +107 -11
  16. data/lib/rubocop/view_component/version.rb +1 -1
  17. data/script/verify +29 -16
  18. data/spec/expected_flowbite_failures.yml +64 -0
  19. data/spec/expected_govuk_failures.yml +10 -12
  20. data/spec/expected_polaris_failures.yml +2 -0
  21. data/spec/expected_primer_failures.yml +60 -2
  22. data/spec/rubocop/cop/view_component/base_spec.rb +108 -0
  23. data/spec/rubocop/cop/view_component/component_suffix_spec.rb +0 -58
  24. data/spec/rubocop/cop/view_component/missing_preview_spec.rb +1 -42
  25. data/spec/rubocop/cop/view_component/prefer_composition_spec.rb +91 -4
  26. data/spec/rubocop/cop/view_component/prefer_private_methods_spec.rb +126 -4
  27. data/spec/rubocop/cop/view_component/test_rendered_output_spec.rb +287 -4
  28. data/spec/spec_helper.rb +10 -0
  29. data/spec/support/project_index_helpers.rb +71 -0
  30. data/verification/flowbite_rubocop_config.yml +12 -0
  31. data/verification/govuk_rubocop_config.yml +3 -6
  32. data/verification/libraries.yml +4 -0
  33. data/verification/polaris_rubocop_config.yml +3 -7
  34. data/verification/primer_rubocop_config.yml +5 -7
  35. metadata +29 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be0a1c1c9a7489ed311953b5f67c7108dbca8b1f43debef36026ec011781a549
4
- data.tar.gz: 0b62b04534ab81725ab96d28c0b1931671d92944277ac7b5217f4d36d9d91f9b
3
+ metadata.gz: ab88d6999ec2a99b6f9e459d22a14b9d727b62edd967088fa22aef709681dbdb
4
+ data.tar.gz: b06b97cebdb888ba95176af4d9d664c9f75bf75865eacb148bc327bba761f77f
5
5
  SHA512:
6
- metadata.gz: 7fc23bef50c35486b422362f9e738e5347b41eaeb8171f2dfca21884dbd87542f2db1e22618c1b239a1bfd2d878b448cb208539678c37b6e622086c1ee4d2871
7
- data.tar.gz: f67e8fc0ec682da84318f7078bc5eaffd4a7e9907e9d4e281a224e240e57a13b4f4ec010ef31cce3f20528b4f6fc4ce5acf538385736a837095ef21a90fcf641
6
+ metadata.gz: 8b6aa5e13ce63d13205335bb2d900a29d57e80ce1b9d1906c92e3548d85941875b18235a36775849e35752fed347c260e842289fb8e805ad0ee7670c252c89b1
7
+ data.tar.gz: b65833f97c6211d366b5c0cdeba807e501f6ef10a8a878f63e0379de23be3c2a4842542e50e43171254886204990ae5224a4d3e3abbe90f72be1e05f9cfb0f08
data/.rubocop.yml CHANGED
@@ -12,10 +12,14 @@ AllCops:
12
12
  TargetRubyVersion: 3.2
13
13
  NewCops: enable
14
14
  SuggestExtensions: false
15
+ # Required by this gem's own cops, which resolve ancestry through the
16
+ # project index (see docs/rubydex.md).
17
+ UseProjectIndex: true
15
18
  Exclude:
16
19
  - 'verification/**/*'
17
20
  - 'script/verify'
18
21
  - 'spec/fixtures/**/*'
22
+ - 'gemfiles/**/*'
19
23
 
20
24
  Metrics/BlockLength:
21
25
  Exclude:
@@ -1,6 +1,6 @@
1
- # CLAUDE.md
1
+ # AGENTS.md
2
2
 
3
- This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
3
+ This file provides guidance to coding agents when working with code in this repository.
4
4
 
5
5
  ## Project Overview
6
6
 
@@ -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](docs/rubydex.md)
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/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/README.md CHANGED
@@ -13,9 +13,19 @@ gem 'rubocop-view_component', require: false
13
13
  Add to your `.rubocop.yml`:
14
14
 
15
15
  ```yaml
16
- require:
16
+ plugins:
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
 
@@ -25,47 +35,40 @@ This gem provides several cops to enforce ViewComponent best practices:
25
35
  - **ViewComponent/NoGlobalState** - Prevent direct access to `params`, `request`, `session`, etc.
26
36
  - **ViewComponent/PreferPrivateMethods** - Suggest making helper methods private (analyzes ERB templates to avoid flagging methods used in views)
27
37
  - **ViewComponent/PreferSlots** - Detect HTML parameters that should be slots
28
- - **ViewComponent/PreferComposition** - Avoid inheriting one ViewComponent from another (prefer composition)
29
- - **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.
38
+ - **ViewComponent/PreferComposition** - Avoid inheriting one ViewComponent from another (prefer composition). Abstract bases without their own template, such as `ApplicationComponent`, are allowed
39
+ - **ViewComponent/TestRenderedOutput** - Encourage testing rendered output over private methods. Supports Minitest and RSpec styles, including components built via `described_class.new` or a `let`/`subject` helper
40
+ - **ViewComponent/MissingPreview** - Ensure every ViewComponent has a corresponding preview file (defaults to `test/components/previews` and `spec/components/previews` if `PreviewPaths` is not configured). 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`. To add extra parent classes, use `inherit_mode: merge` so the defaults are preserved:
46
+ Several cops (`ComponentSuffix`, `PreferComposition`, `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
- ViewComponent:
41
- inherit_mode:
42
- merge:
43
- - ViewComponentParentClasses
44
- ViewComponentParentClasses:
45
- - MyApp::BaseComponent
50
+ ViewComponent/ComponentSuffix:
51
+ Include:
52
+ - 'app/components/**/*.rb'
53
+ - 'engines/*/app/components/**/*.rb'
46
54
  ```
47
55
 
48
- To replace the defaults entirely (e.g. if your project has renamed `ApplicationComponent`), omit `inherit_mode`:
56
+ ### Additional Source Paths
57
+
58
+ `AllCops/ProjectIndexIncludesGems` only indexes a gem's `lib`, so a class that inherits through a
59
+ Rails engine's `app/components` stays unresolved. If your components inherit from an engine gem,
60
+ point the cops at its sources:
49
61
 
50
62
  ```yaml
51
63
  # .rubocop.yml
52
64
  ViewComponent:
53
- ViewComponentParentClasses:
54
- - ViewComponent::Base
55
- - MyApp::BaseComponent
65
+ AdditionalSourcePaths:
66
+ - ../my_engine/app/components
56
67
  ```
57
68
 
58
- ### Components Directory
59
-
60
- 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`:
61
-
62
- ```yaml
63
- # .rubocop.yml
64
- ViewComponent/ComponentSuffix:
65
- Include:
66
- - 'app/components/**/*.rb'
67
- - 'engines/*/app/components/**/*.rb'
68
- ```
69
+ Paths are relative to the config file that sets them. The cops index those directories themselves
70
+ and fall back to them when RuboCop's index can't resolve a constant. Unlike `ComponentNamespaces`,
71
+ this only adds declarations to the index — it does not treat a whole namespace as components.
69
72
 
70
73
  ### No Super
71
74
 
@@ -157,6 +160,26 @@ To force download the latest Polaris source:
157
160
  script/verify polaris --update
158
161
  ```
159
162
 
163
+ ### Flowbite Components
164
+
165
+ To verify against [substancelab/flowbite-components](https://github.com/substancelab/flowbite-components) locally:
166
+
167
+ ```bash
168
+ script/verify flowbite
169
+ ```
170
+
171
+ If you intentionally change cop behavior, regenerate the snapshot:
172
+
173
+ ```bash
174
+ script/verify flowbite --regenerate
175
+ ```
176
+
177
+ To force download the latest Flowbite source:
178
+
179
+ ```bash
180
+ script/verify flowbite --update
181
+ ```
182
+
160
183
  ## Contributing
161
184
 
162
185
  Bug reports and pull requests are welcome on GitHub at https://github.com/andyw8/rubocop-view_component.
data/config/default.yml CHANGED
@@ -1,8 +1,11 @@
1
1
  ViewComponent:
2
- ViewComponentParentClasses:
3
- - ViewComponent::Base
4
- - ApplicationComponent
5
2
  ComponentNamespaces: []
3
+ # Source directories the cops index themselves, in addition to RuboCop's
4
+ # project index. Use this when ancestry passes through a gem RuboCop does not
5
+ # index (notably a Rails engine's app/components, which
6
+ # AllCops/ProjectIndexIncludesGems misses because it only covers a gem's lib).
7
+ # Paths are relative to the config file that sets them.
8
+ AdditionalSourcePaths: []
6
9
 
7
10
  ViewComponent/ComponentSuffix:
8
11
  Description: 'Enforce -Component suffix for ViewComponent classes.'
@@ -18,8 +21,6 @@ ViewComponent/MissingPreview:
18
21
  Enabled: true
19
22
  VersionAdded: '0.5.0'
20
23
  Severity: convention
21
- Include:
22
- - 'app/components/**/*_component.rb'
23
24
  PreviewPaths:
24
25
  - test/components/previews
25
26
  - spec/components/previews
data/docs/rubydex.md ADDED
@@ -0,0 +1,136 @@
1
+ # rubydex integration
2
+
3
+ This gem uses [rubydex](https://github.com/Shopify/rubydex), Shopify's high-performance
4
+ static analysis toolkit for Ruby, to answer questions that cannot be answered from a
5
+ single file's AST.
6
+
7
+ Most ViewComponent cops only care about *ancestry*: is this class (or the class it
8
+ inherits from) ultimately a `ViewComponent::Base` descendant? A class may inherit from
9
+ `ApplicationComponent`, which inherits from a gem's base class, which finally inherits
10
+ from `ViewComponent::Base` — none of which is visible in the file being inspected.
11
+ Resolving that chain requires a project-wide index, and that is what rubydex provides.
12
+
13
+ ## How RuboCop exposes the index
14
+
15
+ RuboCop 1.89+ integrates rubydex behind an opt-in flag. This gem requires that
16
+ version (see `rubocop-view_component.gemspec`).
17
+
18
+ 1. When `AllCops/UseProjectIndex` is `true`, RuboCop builds a resolved
19
+ `Rubydex::Graph` for the whole project (`RuboCop::ProjectIndexLoader`).
20
+ 2. The graph is exposed to cops through `RuboCop::Cop::Base#project_index`.
21
+ 3. Cops that mix in `RuboCop::Cop::ProjectIndexHelp` also get an
22
+ `external_dependency_checksum` override, which invalidates RuboCop's result cache
23
+ whenever an indexed file changes on disk.
24
+
25
+ If the `rubydex` gem is not installed, or the running Ruby is older than 3.2, RuboCop
26
+ warns and degrades to file-local analysis (`project_index` is `nil`).
27
+
28
+ ## How this gem uses it
29
+
30
+ `ViewComponent::Base` (`lib/rubocop/cop/view_component/base.rb`) includes
31
+ `ProjectIndexHelp` and queries the index through `resolve_constant_in_index`:
32
+
33
+ - `view_component_class?` — resolves a class's superclass expression and checks it.
34
+ - `view_component_parent?` — resolves a constant and calls
35
+ `declaration.has_ancestor?("ViewComponent::Base")`.
36
+ - `view_component_parent_class?` — checks `has_ancestor?` plus `declaration.descendants`
37
+ to detect abstract base classes that have their own subclasses in the project.
38
+ - `resolved_index_declaration` — returns the raw declaration for a constant, or nil
39
+ when it isn't defined in the index. This lets a cop distinguish "resolved but not a
40
+ ViewComponent" from "not resolvable", and inspect the declaration (for example, to
41
+ locate the class's source file).
42
+ - `inside_view_component?` — finds the enclosing class and delegates to
43
+ `view_component_class?`.
44
+
45
+ `PreferComposition` uses `resolved_index_declaration` together with the parent's
46
+ template to tell a concrete component apart from an abstract base: it flags a parent
47
+ only when its name follows the component convention *and* the index resolves it to a
48
+ ViewComponent that has its own template. Bases without a template (such as
49
+ `ApplicationComponent`) and constants that resolve to something other than a
50
+ ViewComponent are allowed.
51
+
52
+ All of these call `require_project_index!` first, which raises with an actionable
53
+ message if the index is unavailable:
54
+
55
+ ```
56
+ ViewComponent cops require `AllCops/UseProjectIndex: true` in your .rubocop.yml
57
+ for ancestry-based class detection.
58
+ ```
59
+
60
+ The one exception is `ComponentNamespaces`: if a class's fully-qualified name matches a
61
+ configured namespace prefix, `view_component_class?` can return `true` without touching
62
+ the index, so projects that use explicit namespaces can avoid the requirement in that
63
+ specific case.
64
+
65
+ ## Configuration
66
+
67
+ Enable the index in your `.rubocop.yml`:
68
+
69
+ ```yaml
70
+ AllCops:
71
+ UseProjectIndex: true
72
+ ```
73
+
74
+ If your components inherit from a library (e.g. Primer) whose base class lives in a gem,
75
+ also enable gem indexing so the ancestry chain can be followed through the gem's source:
76
+
77
+ ```yaml
78
+ AllCops:
79
+ ProjectIndexIncludesGems: true
80
+ ```
81
+
82
+ ### Rails engines
83
+
84
+ Gem indexing only covers a gem's `lib`. A Rails engine's components live under `app/components`,
85
+ so a chain that passes through one — such as a project's components inheriting
86
+ `MyEngine::ApplicationComponent` — stays unresolved even with the flag on. List the engine's
87
+ sources in `ViewComponent/AdditionalSourcePaths` and the cops index them in a small supplementary
88
+ graph of their own, used as a fallback when RuboCop's index can't resolve a constant:
89
+
90
+ ```yaml
91
+ ViewComponent:
92
+ AdditionalSourcePaths:
93
+ - ../my_engine/app/components
94
+ ```
95
+
96
+ Paths are relative to the config file that sets them, and the supplementary files take part in the
97
+ result cache's checksum, so changing one invalidates a cached run. `ComponentNamespaces` is not a
98
+ substitute here: it treats *every* class under the namespace as a component, which is wrong for a
99
+ namespace that also holds helpers, tests, or other application code.
100
+
101
+ ## Performance notes
102
+
103
+ - The index always covers the whole project (rooted at the directory of the config that
104
+ enabled it), not just the files in the current run, so cross-file offenses are
105
+ consistent between full runs, editor runs, and CI shards.
106
+ - `external_dependency_checksum` ties the result cache to the indexed files' mtimes and
107
+ sizes, so a warm cache is reused until a relevant file changes.
108
+ - On Windows, parallel inspection is skipped when the index is enabled.
109
+
110
+ ## Testing
111
+
112
+ Specs do not build a full RuboCop run. `spec/support/project_index_helpers.rb` builds a
113
+ `Rubydex::Graph` directly from a set of in-memory sources:
114
+
115
+ ```ruby
116
+ graph = Rubydex::Graph.new
117
+ graph.index_source("file:///user_component.rb", source, "ruby")
118
+ graph.resolve
119
+ ```
120
+
121
+ `ProjectIndexHelpers::LazyIndex` wraps the graph and lazily indexes the source under
122
+ inspection on first miss, mirroring how the real runner indexes every project file
123
+ (including the one being inspected). `spec_helper.rb` assigns this to each cop's
124
+ `project_index` before examples run.
125
+
126
+ The real-world verification suites (`verification/*_rubocop_config.yml`) set
127
+ `UseProjectIndex: true` so the cops are exercised through the actual RuboCop runner.
128
+
129
+ ## Further reading
130
+
131
+ - rubydex repository: <https://github.com/Shopify/rubydex>
132
+ - Ruby API documentation: <https://shopify.github.io/rubydex/>
133
+ - "One engine, many tools — Introducing Rubydex":
134
+ <https://railsatscale.com/2026-05-12-one-engine-many-tools/>
135
+ - RuboCop 1.89 project-wide analysis announcement:
136
+ <https://metaredux.com/posts/2026/08/05/rubocop-1-89.html>
@@ -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,63 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rubydex"
4
+ require_relative "supplementary_index"
5
+
3
6
  module RuboCop
4
7
  module Cop
5
8
  module ViewComponent
6
- # Shared helper methods for ViewComponent cops
9
+ # Shared helper methods for ViewComponent cops.
10
+ # Requires rubydex with UseProjectIndex: true for ancestry resolution.
7
11
  module Base
8
- # Check if a class node inherits from ViewComponent::Base or ApplicationComponent
12
+ include ProjectIndexHelp
13
+ include SupplementaryIndex
14
+
15
+ VC_BASE = "ViewComponent::Base"
16
+
17
+ # Check if a class node inherits from ViewComponent::Base
9
18
  def view_component_class?(node)
10
19
  return false unless node&.class_type?
11
20
 
12
21
  class_source = fully_qualified_name(node)
13
22
  return true if component_namespaces.any? { |ns| class_source.start_with?(ns) }
14
23
 
24
+ require_project_index!
25
+
15
26
  parent_class = node.parent_class
16
27
  return false unless parent_class
17
28
 
18
29
  view_component_parent?(parent_class)
19
30
  end
20
31
 
21
- # Check if node represents a configured parent class
32
+ # Check if a constant node resolves to a class with ViewComponent::Base as ancestor
22
33
  def view_component_parent?(node)
23
- return false unless node.const_type?
34
+ declaration = view_component_declaration(node)
35
+ return false unless declaration
24
36
 
25
- (cop_config["ViewComponentParentClasses"] || []).include?(node.source)
37
+ declaration.name == VC_BASE || declaration.has_ancestor?(VC_BASE)
26
38
  end
27
39
 
28
- # Check if a class node is itself one of the registered parent classes.
40
+ # Check if a class node is itself an abstract parent class
41
+ # (has ViewComponent::Base as ancestor and has descendants in the project)
29
42
  def view_component_parent_class?(node)
30
43
  return false unless node&.class_type?
31
44
 
32
- (cop_config["ViewComponentParentClasses"] || []).include?(fully_qualified_name(node))
45
+ declaration = view_component_declaration(node.identifier)
46
+ return false unless declaration
47
+
48
+ declaration.has_ancestor?(VC_BASE) &&
49
+ declaration.descendants.any? { |d| d.name != declaration.name }
50
+ end
51
+
52
+ # Resolve a constant node to its declaration in the project index, or
53
+ # nil when the node is not a constant or isn't defined anywhere in the
54
+ # index. The declaration may or may not be a ViewComponent; callers can
55
+ # inspect it (for example, to locate the class's source file).
56
+ def resolved_index_declaration(node)
57
+ return nil unless node&.const_type?
58
+
59
+ require_project_index!
60
+ resolve_constant_in_index(node)
33
61
  end
34
62
 
35
63
  def fully_qualified_name(node)
@@ -51,11 +79,32 @@ module RuboCop
51
79
 
52
80
  # Check if node is within a ViewComponent class
53
81
  def inside_view_component?(node)
82
+ require_project_index!
83
+
54
84
  klass = enclosing_class(node)
55
85
  return false unless klass
56
86
 
57
87
  view_component_class?(klass)
58
88
  end
89
+
90
+ def require_project_index!
91
+ return if project_index
92
+
93
+ raise <<~MSG
94
+ ViewComponent cops require `AllCops/UseProjectIndex: true` in your .rubocop.yml
95
+ for ancestry-based class detection.
96
+ MSG
97
+ end
98
+
99
+ private
100
+
101
+ # Resolve a constant node to a namespace declaration in the project
102
+ # index, or nil when the node is not a constant or does not resolve to a
103
+ # class or module.
104
+ def view_component_declaration(node)
105
+ declaration = resolved_index_declaration(node)
106
+ declaration if declaration.respond_to?(:has_ancestor?)
107
+ end
59
108
  end
60
109
  end
61
110
  end