rubocop-vicenzo 0.3.0 → 0.4.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/CHANGELOG.md +6 -0
- data/README.md +21 -0
- data/antora-playbook.yml +15 -0
- data/config/default.yml +23 -0
- data/docs/antora.yml +5 -0
- data/docs/supplemental-ui/partials/header-content.hbs +14 -0
- data/lib/rubocop/cop/vicenzo/rails/enum_inclusion_of_validation.rb +10 -17
- data/lib/rubocop/cop/vicenzo/rspec/conditional_in_spec.rb +72 -0
- data/lib/rubocop/cop/vicenzo/rspec/dynamic_example_generation.rb +89 -0
- data/lib/rubocop/cop/vicenzo/rspec/iteration_inside_example.rb +84 -0
- data/lib/rubocop/cop/vicenzo_cops.rb +3 -0
- data/lib/rubocop/vicenzo/version.rb +1 -1
- data/rakelib/docs.rake +227 -0
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2caba14e7971e328ef6fc5c5fb203fe11a26cbd1f78f534f223f01f9d99c240f
|
|
4
|
+
data.tar.gz: 2eb689487018dfa035aff365831cc219cf71dfce583885639656f314fbcffc82
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e1c048efe4a3c8b7f7de35d8fcbe76908efcceb50bf053df761e59d53ea8fa9d74f779f223bb0d6e6ff1274e4ec2c83daf9a561084f970cdd5a48c8b7d4f7dce
|
|
7
|
+
data.tar.gz: f8c2f91bd54d5845ae68928a5ff783f6b161a47ca0e2e1d0afdc177473761416487d2e0d5c984f9b6d064a98ef3cc64db98baf3454f3989be19bb146172a3620
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-03-28
|
|
4
|
+
|
|
5
|
+
- Add RuboCop::Cop::Vicenzo::RSpec::ConditionalInSpec #19;
|
|
6
|
+
- Add RuboCop::Cop::Vicenzo::RSpec::DynamicExampleGeneration #19;
|
|
7
|
+
- Add RuboCop::Cop::Vicenzo::RSpec::IterationInsideExample #19;
|
|
8
|
+
|
|
3
9
|
## [0.3.0] - 2025-12-17
|
|
4
10
|
|
|
5
11
|
- Add RuboCop::Cop::Vicenzo::Layout::MultilineMethodCallLineBreaks #12;
|
data/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/bvicenzo/rubocop-vicenzo/actions/workflows/main.yml)
|
|
4
4
|
|
|
5
|
+
📖 **[Documentation](https://bvicenzo.github.io/rubocop-vicenzo)**
|
|
6
|
+
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
7
9
|
Install the gem and add to the application's Gemfile by executing:
|
|
@@ -55,6 +57,25 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
|
55
57
|
|
|
56
58
|
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).
|
|
57
59
|
|
|
60
|
+
### Documentation
|
|
61
|
+
|
|
62
|
+
The documentation site is built with [Antora](https://antora.org) and published automatically to GitHub Pages on every new release.
|
|
63
|
+
|
|
64
|
+
To build it locally, you will need [Node.js](https://nodejs.org) (v20+) installed. Then install Antora:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
npm install -g @antora/cli @antora/site-generator
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Generate the AsciiDoc pages from the cop sources and build the site:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
bundle exec rake docs:generate
|
|
74
|
+
antora antora-playbook.yml
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The site will be available at `build/site/index.html`.
|
|
78
|
+
|
|
58
79
|
### Generate binstubs
|
|
59
80
|
|
|
60
81
|
If you want is possible change the command `bundle exec something` by `bin/something` generating binstubs
|
data/antora-playbook.yml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
site:
|
|
2
|
+
title: RuboCop Vicenzo
|
|
3
|
+
url: https://bvicenzo.github.io/rubocop-vicenzo
|
|
4
|
+
|
|
5
|
+
content:
|
|
6
|
+
sources:
|
|
7
|
+
- url: .
|
|
8
|
+
branches: HEAD
|
|
9
|
+
start_path: docs
|
|
10
|
+
|
|
11
|
+
ui:
|
|
12
|
+
bundle:
|
|
13
|
+
url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/HEAD/raw/build/ui-bundle.zip?job=bundle-stable
|
|
14
|
+
snapshot: true
|
|
15
|
+
supplemental_files: ./docs/supplemental-ui
|
data/config/default.yml
CHANGED
|
@@ -11,12 +11,35 @@ Vicenzo/Rails/EnumInclusionOfValidation:
|
|
|
11
11
|
Severity: convention
|
|
12
12
|
VersionAdded: '0.1.0'
|
|
13
13
|
|
|
14
|
+
Vicenzo/RSpec/ConditionalInSpec:
|
|
15
|
+
Description: 'Do not use conditional logic in specs. Extract each branch into an explicit context instead.'
|
|
16
|
+
Enabled: true
|
|
17
|
+
Severity: warning
|
|
18
|
+
Include:
|
|
19
|
+
- '**/spec/**/*_spec.rb'
|
|
20
|
+
Exclude:
|
|
21
|
+
- '**/spec/support/**/*'
|
|
22
|
+
- '**/spec/factories/**/*'
|
|
23
|
+
VersionAdded: '0.4.0'
|
|
24
|
+
|
|
25
|
+
Vicenzo/RSpec/DynamicExampleGeneration:
|
|
26
|
+
Description: 'Do not use iteration to dynamically generate example groups or examples.'
|
|
27
|
+
Enabled: true
|
|
28
|
+
Severity: warning
|
|
29
|
+
VersionAdded: '0.4.0'
|
|
30
|
+
|
|
14
31
|
Vicenzo/RSpec/InconsistentSiblingStructure:
|
|
15
32
|
Description: 'Enforces strict structural consistency (e.g. prevents mixing describe with context or examples with groups).'
|
|
16
33
|
Enabled: true
|
|
17
34
|
Severity: warning
|
|
18
35
|
VersionAdded: '0.2.0'
|
|
19
36
|
|
|
37
|
+
Vicenzo/RSpec/IterationInsideExample:
|
|
38
|
+
Description: 'Do not call `expect` inside an iteration within an example.'
|
|
39
|
+
Enabled: true
|
|
40
|
+
Severity: warning
|
|
41
|
+
VersionAdded: '0.4.0'
|
|
42
|
+
|
|
20
43
|
Vicenzo/RSpec/NestedContextImproperStart:
|
|
21
44
|
Description: 'Check if the nested context does not start as a root one.'
|
|
22
45
|
Enabled: true
|
data/docs/antora.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<header class="header">
|
|
2
|
+
<nav class="navbar">
|
|
3
|
+
<div class="navbar-brand">
|
|
4
|
+
<a class="navbar-item" href="{{{or site.url siteRootPath}}}">{{site.title}}</a>
|
|
5
|
+
{{#if env.SITE_SEARCH_PROVIDER}}
|
|
6
|
+
<div class="navbar-item search hide-for-print">
|
|
7
|
+
<div id="search-field" class="field">
|
|
8
|
+
<input id="search-input" type="text" placeholder="Search the docs"{{#if page.home}} autofocus{{/if}}>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
{{/if}}
|
|
12
|
+
</div>
|
|
13
|
+
</nav>
|
|
14
|
+
</header>
|
|
@@ -7,27 +7,20 @@ module RuboCop
|
|
|
7
7
|
# Ensures that enums using the new syntax include the
|
|
8
8
|
# `validate: { allow_nil: true }` option.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
10
|
+
# Old-style enums (keyword argument syntax) are ignored.
|
|
11
11
|
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad — missing validate option
|
|
14
|
+
# enum :status, { active: 1, inactive: 0 }, suffix: true
|
|
15
15
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# ```
|
|
16
|
+
# # bad — validate option present but incorrect
|
|
17
|
+
# enum :status, { active: 1, inactive: 0 }, validate: true, suffix: true
|
|
19
18
|
#
|
|
20
|
-
#
|
|
19
|
+
# # good
|
|
20
|
+
# enum :status, { active: 1, inactive: 0 }, validate: { allow_nil: true }, suffix: true
|
|
21
21
|
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# ```
|
|
25
|
-
#
|
|
26
|
-
# This cop does not enforce validation on enums using the old syntax:
|
|
27
|
-
#
|
|
28
|
-
# ```ruby
|
|
29
|
-
# enum status: { active: 1, inactive: 0 }
|
|
30
|
-
# ```
|
|
22
|
+
# # ignored — old-style enum syntax
|
|
23
|
+
# enum status: { active: 1, inactive: 0 }
|
|
31
24
|
class EnumInclusionOfValidation < Base
|
|
32
25
|
extend AutoCorrector
|
|
33
26
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vicenzo
|
|
6
|
+
module RSpec
|
|
7
|
+
# Do not use conditional logic in spec files.
|
|
8
|
+
#
|
|
9
|
+
# Any `if`, `unless`, or ternary expression in a spec represents a hidden
|
|
10
|
+
# context. Each branch should be an explicit `context` block so that the
|
|
11
|
+
# conditions and expectations are always clear and unconditional.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# # bad — hidden context inside an example
|
|
15
|
+
#
|
|
16
|
+
# it 'grants or denies access' do
|
|
17
|
+
# if user.admin?
|
|
18
|
+
# expect(result).to eq(:granted)
|
|
19
|
+
# else
|
|
20
|
+
# expect(result).to eq(:denied)
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# # bad — hidden context inside a let
|
|
25
|
+
#
|
|
26
|
+
# let(:user) { admin? ? create(:admin) : create(:client) }
|
|
27
|
+
#
|
|
28
|
+
# # bad — hidden context inside a before hook
|
|
29
|
+
#
|
|
30
|
+
# before { setup_thing if feature_enabled? }
|
|
31
|
+
#
|
|
32
|
+
# # bad — hidden context at the example group level using unless
|
|
33
|
+
#
|
|
34
|
+
# unless legacy_mode?
|
|
35
|
+
# it 'uses the new behaviour' do
|
|
36
|
+
# ...
|
|
37
|
+
# end
|
|
38
|
+
# end
|
|
39
|
+
#
|
|
40
|
+
# # good
|
|
41
|
+
#
|
|
42
|
+
# context 'when user is admin' do
|
|
43
|
+
# let(:user) { create(:admin) }
|
|
44
|
+
#
|
|
45
|
+
# it 'grants access' do
|
|
46
|
+
# expect(result).to eq(:granted)
|
|
47
|
+
# end
|
|
48
|
+
# end
|
|
49
|
+
#
|
|
50
|
+
# context 'when user is not admin' do
|
|
51
|
+
# let(:user) { create(:client) }
|
|
52
|
+
#
|
|
53
|
+
# it 'denies access' do
|
|
54
|
+
# expect(result).to eq(:denied)
|
|
55
|
+
# end
|
|
56
|
+
# end
|
|
57
|
+
class ConditionalInSpec < RuboCop::Cop::RSpec::Base
|
|
58
|
+
MSG = 'Do not use conditional logic in specs. ' \
|
|
59
|
+
'Extract each branch into an explicit context instead.'
|
|
60
|
+
|
|
61
|
+
# Both `if` and `unless` are represented as `if` nodes in the AST,
|
|
62
|
+
# so this single hook covers all conditional forms: `if`, `unless`,
|
|
63
|
+
# modifier `if`/`unless`, and ternary `?:`.
|
|
64
|
+
def on_if(node)
|
|
65
|
+
offense_location = node.ternary? ? node : node.loc.keyword
|
|
66
|
+
add_offense(offense_location)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vicenzo
|
|
6
|
+
module RSpec
|
|
7
|
+
# Do not use iteration to dynamically generate example groups or examples.
|
|
8
|
+
#
|
|
9
|
+
# Dynamic generation makes tests hard to find, hard to read, and creates
|
|
10
|
+
# pressure to add conditional logic (e.g. `if variable == :x`) when not
|
|
11
|
+
# all iterations share the same conditions. Write explicit, static contexts
|
|
12
|
+
# instead — one context per case.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# # bad
|
|
16
|
+
#
|
|
17
|
+
# [:admin, :driver].each do |role|
|
|
18
|
+
# context "when role is #{role}" do
|
|
19
|
+
# it 'does something' do
|
|
20
|
+
# ...
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# # good
|
|
26
|
+
#
|
|
27
|
+
# context 'when role is admin' do
|
|
28
|
+
# let(:role) { :admin }
|
|
29
|
+
#
|
|
30
|
+
# it 'does something' do
|
|
31
|
+
# ...
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# context 'when role is driver' do
|
|
36
|
+
# let(:role) { :driver }
|
|
37
|
+
#
|
|
38
|
+
# it 'does something' do
|
|
39
|
+
# ...
|
|
40
|
+
# end
|
|
41
|
+
# end
|
|
42
|
+
class DynamicExampleGeneration < RuboCop::Cop::RSpec::Base
|
|
43
|
+
MSG = 'Do not use iteration to dynamically generate example groups or examples. ' \
|
|
44
|
+
'Write explicit, static contexts instead.'
|
|
45
|
+
|
|
46
|
+
ENUMERATION_METHODS = %i[each each_with_index each_with_object map flat_map].freeze
|
|
47
|
+
|
|
48
|
+
EXAMPLE_GROUP_DSL = %i[
|
|
49
|
+
context describe feature experiment
|
|
50
|
+
it specify example scenario focus
|
|
51
|
+
let let! subject subject! before after around
|
|
52
|
+
shared_examples shared_context shared_examples_for
|
|
53
|
+
].freeze
|
|
54
|
+
|
|
55
|
+
# @!method enumeration_block?(node)
|
|
56
|
+
def_node_matcher :enumeration_block?, <<~PATTERN
|
|
57
|
+
(block
|
|
58
|
+
(send _ {#{ENUMERATION_METHODS.map(&:inspect).join(' ')}} ...)
|
|
59
|
+
...)
|
|
60
|
+
PATTERN
|
|
61
|
+
|
|
62
|
+
# @!method example_group_dsl_call?(node)
|
|
63
|
+
def_node_matcher :example_group_dsl_call?, <<~PATTERN
|
|
64
|
+
(block (send nil? {#{EXAMPLE_GROUP_DSL.map(&:inspect).join(' ')}} ...) ...)
|
|
65
|
+
PATTERN
|
|
66
|
+
|
|
67
|
+
def on_block(node)
|
|
68
|
+
return unless enumeration_block?(node)
|
|
69
|
+
return unless contains_example_group_dsl?(node)
|
|
70
|
+
|
|
71
|
+
add_offense(node.send_node)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
alias on_numblock on_block
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def contains_example_group_dsl?(node)
|
|
79
|
+
node.body&.each_node(:block) do |child|
|
|
80
|
+
return true if example_group_dsl_call?(child)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
false
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vicenzo
|
|
6
|
+
module RSpec
|
|
7
|
+
# Do not use `expect` inside an iteration within an example.
|
|
8
|
+
#
|
|
9
|
+
# Placing `expect` calls inside an iteration block (e.g. `each`) makes
|
|
10
|
+
# tests implicit and hard to debug: when the assertion fails it is unclear
|
|
11
|
+
# which element caused the failure, and not all elements may represent the
|
|
12
|
+
# same condition. Using iteration to build or transform data before calling
|
|
13
|
+
# `expect` is fine; the problem is calling `expect` inside the iteration.
|
|
14
|
+
# Write explicit assertions for each relevant case instead.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # bad — expect is called inside the iteration
|
|
18
|
+
#
|
|
19
|
+
# it 'returns vehicle costs general values' do
|
|
20
|
+
# response_body[:vehicle_costs].first.each do |attribute, value|
|
|
21
|
+
# expect(value).to eq(vehicle_cost.send(attribute).to_s)
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# # good — iteration builds data, expect is called once outside
|
|
26
|
+
#
|
|
27
|
+
# it 'returns the expected column names' do
|
|
28
|
+
# columns = VehicleCost.column_names.map { |column| column.gsub('_centavos', '') }
|
|
29
|
+
# expect(response_body[:vehicle_costs].first.keys).to match_array(columns.map(&:to_sym))
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# # good — each attribute has an explicit example
|
|
33
|
+
#
|
|
34
|
+
# it 'returns the correct name' do
|
|
35
|
+
# expect(response_body[:vehicle_costs].first[:name]).to eq(vehicle_cost.name)
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# it 'returns the correct value' do
|
|
39
|
+
# expect(response_body[:vehicle_costs].first[:value]).to eq(vehicle_cost.value.to_s)
|
|
40
|
+
# end
|
|
41
|
+
class IterationInsideExample < RuboCop::Cop::RSpec::Base
|
|
42
|
+
MSG = 'Do not call `expect` inside an iteration. ' \
|
|
43
|
+
'Write explicit assertions instead.'
|
|
44
|
+
|
|
45
|
+
ENUMERATION_METHODS = %i[each each_with_index each_with_object].freeze
|
|
46
|
+
|
|
47
|
+
# @!method enumeration_block?(node)
|
|
48
|
+
def_node_matcher :enumeration_block?, <<~PATTERN
|
|
49
|
+
(block
|
|
50
|
+
(send _ {#{ENUMERATION_METHODS.map(&:inspect).join(' ')}} ...)
|
|
51
|
+
...)
|
|
52
|
+
PATTERN
|
|
53
|
+
|
|
54
|
+
def on_block(node)
|
|
55
|
+
return unless example?(node)
|
|
56
|
+
|
|
57
|
+
find_iterations_with_assertions(node.body)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
alias on_numblock on_block
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def find_iterations_with_assertions(body)
|
|
65
|
+
return unless body
|
|
66
|
+
|
|
67
|
+
body.each_node(:block) do |iteration|
|
|
68
|
+
next unless enumeration_block?(iteration)
|
|
69
|
+
next unless contains_expectation?(iteration.body)
|
|
70
|
+
|
|
71
|
+
add_offense(iteration.send_node)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def contains_expectation?(node)
|
|
76
|
+
return false unless node
|
|
77
|
+
|
|
78
|
+
node.each_node(:send).any? { |send_node| send_node.method?(:expect) }
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'vicenzo/rspec/conditional_in_spec'
|
|
4
|
+
require_relative 'vicenzo/rspec/dynamic_example_generation'
|
|
3
5
|
require_relative 'vicenzo/rspec/inconsistent_sibling_structure'
|
|
6
|
+
require_relative 'vicenzo/rspec/iteration_inside_example'
|
|
4
7
|
require_relative 'vicenzo/rspec/nested_context_improper_start'
|
|
5
8
|
require_relative 'vicenzo/rspec/nested_let_redefinition'
|
|
6
9
|
require_relative 'vicenzo/rspec/nested_subject_redefinition'
|
data/rakelib/docs.rake
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
DOCS_PAGES_DIR = 'docs/modules/ROOT/pages'
|
|
7
|
+
DOCS_NAV_FILE = 'docs/modules/ROOT/nav.adoc'
|
|
8
|
+
|
|
9
|
+
CAMEL_BOUNDARIES_UPPER_PATTERN = /(?<leading_caps>[A-Z]+)(?<next_word>[A-Z][a-z])/
|
|
10
|
+
CAMEL_BOUNDARIES_LOWER_PATTERN = /(?<word_end>[a-z\d])(?<word_start>[A-Z])/
|
|
11
|
+
CLASS_DECLARATION_PATTERN = /^\s+class\s/
|
|
12
|
+
COMMENT_LINE_PATTERN = /^\s+#/
|
|
13
|
+
EXAMPLE_TAG_PATTERN = /^\s+#\s+@example(.*)/
|
|
14
|
+
EXAMPLE_CODE_INDENT_PATTERN = /^\s+#\s{0,3}/
|
|
15
|
+
ANCHOR_INVALID_CHARS_PATTERN = /[^a-z0-9-]/
|
|
16
|
+
|
|
17
|
+
namespace :docs do
|
|
18
|
+
desc 'Generate AsciiDoc documentation for all Vicenzo cops'
|
|
19
|
+
task :generate do
|
|
20
|
+
FileUtils.mkdir_p(DOCS_PAGES_DIR)
|
|
21
|
+
|
|
22
|
+
default_config = YAML.load_file('config/default.yml')
|
|
23
|
+
cop_data = default_config.keys.map { |name| build_cop_data(name, default_config) }
|
|
24
|
+
|
|
25
|
+
cops_by_dept = cop_data.group_by { |cop| cop[:department] }
|
|
26
|
+
|
|
27
|
+
cops_by_dept.sort.each do |department, cops|
|
|
28
|
+
content = render_department_page(department, cops.sort_by { |cop| cop[:name] })
|
|
29
|
+
filename = "cops_#{department.downcase}.adoc"
|
|
30
|
+
File.write(File.join(DOCS_PAGES_DIR, filename), content)
|
|
31
|
+
puts " Generated: #{filename} (#{cops.size} cop#{'s' if cops.size != 1})"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
write_index(cop_data)
|
|
35
|
+
write_nav(cops_by_dept)
|
|
36
|
+
puts "\nDone. #{cop_data.size} cops documented across #{cops_by_dept.size} departments."
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Data extraction
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
def build_cop_data(cop_name, default_config)
|
|
45
|
+
config = default_config[cop_name] || {}
|
|
46
|
+
source = read_cop_source(cop_name)
|
|
47
|
+
cop_data_hash(cop_name, config, source)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def cop_data_hash(cop_name, config, source)
|
|
51
|
+
{
|
|
52
|
+
name: cop_name,
|
|
53
|
+
department: cop_name.split('/')[1],
|
|
54
|
+
description: config['Description'] || '',
|
|
55
|
+
version: config['VersionAdded'] || '-',
|
|
56
|
+
enabled: config.fetch('Enabled', true),
|
|
57
|
+
autocorrect: source ? autocorrect?(source) : false,
|
|
58
|
+
examples: source ? extract_examples(source) : [],
|
|
59
|
+
config_keys: extra_config_keys(config)
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def read_cop_source(cop_name)
|
|
64
|
+
path = cop_name_to_path(cop_name)
|
|
65
|
+
File.exist?(path) ? File.read(path) : nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def cop_name_to_path(cop_name)
|
|
69
|
+
parts = cop_name.split('/')
|
|
70
|
+
dept = parts[1].downcase
|
|
71
|
+
klass = underscore(parts[2])
|
|
72
|
+
"lib/rubocop/cop/vicenzo/#{dept}/#{klass}.rb"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def underscore(str)
|
|
76
|
+
str
|
|
77
|
+
.gsub(CAMEL_BOUNDARIES_UPPER_PATTERN, '\k<leading_caps>_\k<next_word>')
|
|
78
|
+
.gsub(CAMEL_BOUNDARIES_LOWER_PATTERN, '\k<word_end>_\k<word_start>')
|
|
79
|
+
.downcase
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def autocorrect?(source)
|
|
83
|
+
source.include?('AutoCorrector')
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def extract_examples(source)
|
|
87
|
+
docstring = extract_docstring(source)
|
|
88
|
+
parse_examples(docstring)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def extract_docstring(source)
|
|
92
|
+
lines = []
|
|
93
|
+
source.each_line do |line|
|
|
94
|
+
break if CLASS_DECLARATION_PATTERN.match?(line)
|
|
95
|
+
|
|
96
|
+
lines << line if COMMENT_LINE_PATTERN.match?(line)
|
|
97
|
+
end
|
|
98
|
+
lines
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def parse_examples(docstring_lines)
|
|
102
|
+
examples = []
|
|
103
|
+
current = nil
|
|
104
|
+
docstring_lines.each do |line|
|
|
105
|
+
examples, current = update_examples(line, examples, current)
|
|
106
|
+
end
|
|
107
|
+
finalize_examples(examples, current)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def update_examples(line, examples, current)
|
|
111
|
+
if line =~ EXAMPLE_TAG_PATTERN
|
|
112
|
+
examples << current if current
|
|
113
|
+
[examples, { title: Regexp.last_match(1).strip, code: [] }]
|
|
114
|
+
elsif current
|
|
115
|
+
current[:code] << line.sub(EXAMPLE_CODE_INDENT_PATTERN, '').rstrip
|
|
116
|
+
[examples, current]
|
|
117
|
+
else
|
|
118
|
+
[examples, current]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def finalize_examples(examples, current)
|
|
123
|
+
examples << current if current
|
|
124
|
+
examples.each { |example| example[:code].pop while example[:code].last&.empty? }
|
|
125
|
+
examples
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def extra_config_keys(config)
|
|
129
|
+
skip = %w[Description Enabled Severity VersionAdded Include Exclude Safe]
|
|
130
|
+
config.except(*skip)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# AsciiDoc rendering
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def render_department_page(department, cops)
|
|
138
|
+
lines = ["= Vicenzo/#{department}", ':toc: left', ':toc-title: Cops', ':toclevels: 1', '']
|
|
139
|
+
cops.each { |cop| lines.concat(render_cop(cop)) }
|
|
140
|
+
lines.join("\n")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def render_cop(cop)
|
|
144
|
+
lines = ["== #{cop[:name]}", '', cop[:description], '']
|
|
145
|
+
lines.concat(render_metadata_table(cop))
|
|
146
|
+
lines.concat(render_all_examples(cop[:examples]))
|
|
147
|
+
lines.concat(render_config_keys(cop[:config_keys])) unless cop[:config_keys].empty?
|
|
148
|
+
lines.push("'''", '')
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def render_all_examples(examples)
|
|
152
|
+
examples.each_with_index.flat_map { |example, index| render_example(example, index) }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def render_metadata_table(cop)
|
|
156
|
+
enabled = cop[:enabled] ? 'Enabled' : 'Disabled'
|
|
157
|
+
autocorrect = cop[:autocorrect] ? 'Yes' : 'No'
|
|
158
|
+
['[cols="1,1,1,1"]', '|===', '| Enabled by default | Safe | Supports autocorrection | Version Added',
|
|
159
|
+
'', "| #{enabled}", '| Yes', "| #{autocorrect}", "| #{cop[:version]}", '|===', '']
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def render_example(example, index)
|
|
163
|
+
title = if example[:title].empty?
|
|
164
|
+
index.zero? ? 'Example' : "Example #{index + 1}"
|
|
165
|
+
else
|
|
166
|
+
example[:title]
|
|
167
|
+
end
|
|
168
|
+
["=== #{title}", '', '[source,ruby]', '----', *example[:code], '----', '']
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def render_config_keys(config_keys)
|
|
172
|
+
lines = ['=== Configurable attributes', '', '[cols="1,1"]', '|===', '| Name | Default value', '']
|
|
173
|
+
config_keys.each { |key, value| lines.push("| #{key}", "| `#{value.inspect}`", '') }
|
|
174
|
+
lines.push('|===', '')
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
# Index and navigation
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
def write_index(cop_data)
|
|
182
|
+
lines = index_header_lines + index_cops_table_lines(cop_data)
|
|
183
|
+
File.write(File.join(DOCS_PAGES_DIR, 'index.adoc'), lines.join("\n"))
|
|
184
|
+
puts ' Generated: index.adoc'
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def index_header_lines
|
|
188
|
+
['= RuboCop Vicenzo', ':toc: left', ''] +
|
|
189
|
+
['Custom RuboCop cops for enforcing conventions adopted by Vicenzo projects.', ''] +
|
|
190
|
+
installation_section_lines +
|
|
191
|
+
['== Cops', '', '[cols="2,1,1"]', '|===', '| Cop | Department | Version Added', '']
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def installation_section_lines
|
|
195
|
+
['== Installation', ''] +
|
|
196
|
+
gemfile_installation_lines +
|
|
197
|
+
rubocop_yml_installation_lines
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def gemfile_installation_lines
|
|
201
|
+
['Add to your `Gemfile`:', '', '[source,ruby]', '----',
|
|
202
|
+
"gem 'rubocop-vicenzo', require: false", '----', '']
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def rubocop_yml_installation_lines
|
|
206
|
+
['Then add to your `.rubocop.yml`:', '', '[source,yaml]', '----',
|
|
207
|
+
'plugins:', ' - rubocop-vicenzo', '----', '']
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def index_cops_table_lines(cop_data)
|
|
211
|
+
lines = cop_data.sort_by { |cop| cop[:name] }.map do |cop|
|
|
212
|
+
dept = cop[:department]
|
|
213
|
+
filename = "cops_#{dept.downcase}.adoc"
|
|
214
|
+
anchor = cop[:name].downcase.tr('/', '-').gsub(ANCHOR_INVALID_CHARS_PATTERN, '')
|
|
215
|
+
"| xref:#{filename}##{anchor}[#{cop[:name]}] | #{dept} | #{cop[:version]}"
|
|
216
|
+
end
|
|
217
|
+
lines.push('|===', '')
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def write_nav(cops_by_dept)
|
|
221
|
+
lines = ['* xref:index.adoc[Home]', '* Cops']
|
|
222
|
+
cops_by_dept
|
|
223
|
+
.sort
|
|
224
|
+
.each { |department, _cops| lines << "** xref:cops_#{department.downcase}.adoc[#{department}]" }
|
|
225
|
+
File.write(DOCS_NAV_FILE, "#{lines.join("\n")}\n")
|
|
226
|
+
puts ' Generated: nav.adoc'
|
|
227
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-vicenzo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bruno Vicenzo
|
|
@@ -66,11 +66,17 @@ files:
|
|
|
66
66
|
- LICENSE.txt
|
|
67
67
|
- README.md
|
|
68
68
|
- Rakefile
|
|
69
|
+
- antora-playbook.yml
|
|
69
70
|
- config/default.yml
|
|
71
|
+
- docs/antora.yml
|
|
72
|
+
- docs/supplemental-ui/partials/header-content.hbs
|
|
70
73
|
- lib/rubocop-vicenzo.rb
|
|
71
74
|
- lib/rubocop/cop/vicenzo/layout/multiline_method_call_line_breaks.rb
|
|
72
75
|
- lib/rubocop/cop/vicenzo/rails/enum_inclusion_of_validation.rb
|
|
76
|
+
- lib/rubocop/cop/vicenzo/rspec/conditional_in_spec.rb
|
|
77
|
+
- lib/rubocop/cop/vicenzo/rspec/dynamic_example_generation.rb
|
|
73
78
|
- lib/rubocop/cop/vicenzo/rspec/inconsistent_sibling_structure.rb
|
|
79
|
+
- lib/rubocop/cop/vicenzo/rspec/iteration_inside_example.rb
|
|
74
80
|
- lib/rubocop/cop/vicenzo/rspec/leaky_definition.rb
|
|
75
81
|
- lib/rubocop/cop/vicenzo/rspec/nested_context_improper_start.rb
|
|
76
82
|
- lib/rubocop/cop/vicenzo/rspec/nested_let_redefinition.rb
|
|
@@ -80,6 +86,7 @@ files:
|
|
|
80
86
|
- lib/rubocop/vicenzo.rb
|
|
81
87
|
- lib/rubocop/vicenzo/plugin.rb
|
|
82
88
|
- lib/rubocop/vicenzo/version.rb
|
|
89
|
+
- rakelib/docs.rake
|
|
83
90
|
- rakelib/release.rake
|
|
84
91
|
- sig/rubocop/vicenzo.rbs
|
|
85
92
|
homepage: https://github.com/bvicenzo/rubocop-vicenzo
|
|
@@ -106,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
106
113
|
- !ruby/object:Gem::Version
|
|
107
114
|
version: '0'
|
|
108
115
|
requirements: []
|
|
109
|
-
rubygems_version:
|
|
116
|
+
rubygems_version: 4.0.3
|
|
110
117
|
specification_version: 4
|
|
111
118
|
summary: Cops of Bruno Vicenzo
|
|
112
119
|
test_files: []
|