rubocop-vicenzo 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93a0df32e2fd3e164d0b7380c341fe12eb61a46143f4ec15f990961d72d1a611
4
- data.tar.gz: ff138db061137acee6d7067d4cb9e3301d36f6ea33dac8cd494d9a616c8c2ac6
3
+ metadata.gz: b430ab160562cd140cd498e015dbf4e2c03a6bd99189ae24adc00881a3bbab48
4
+ data.tar.gz: 3757afc640413d2ed6548319003ecf62be4cc84658673d430e5ff8236936f2d6
5
5
  SHA512:
6
- metadata.gz: 0b5ddd5178c5866f85d86624f5709cb52a51444b2077363e2a73242feaeb80933e435e8009edbff123e786f01a671e7b7e0d8946e1e06096cb7dc747967f9ab1
7
- data.tar.gz: d79145ff57444fa82a3a4dd89375821b77755841eeac1b373698784bab36a7c4448f609c4733de6b1206460684d3db3ab423b234ef661ad423ac47ea4df38c65
6
+ metadata.gz: 4b2cbdc348b8faa2dd97e722e78e5c8956b67da26df8541314990331a645b156c758540d6602960e8f96340a6294bf0a35a833b4f2d5a1a237349bbb91f58ba6
7
+ data.tar.gz: 88b77a6119a722404ad6d263f61ed53423e4991ae216bd8ea2295712fa72c5536b902948bea8c61e2f32e832444e356c2a0b16ae2f4a8727993acd063db40130
data/.rubocop.yml CHANGED
@@ -25,3 +25,8 @@ RSpec/ExampleLength:
25
25
 
26
26
  RSpec/NestedGroups:
27
27
  Enabled: false
28
+
29
+ Vicenzo/RSpec/LeakyDefinition:
30
+ Exclude:
31
+ - 'spec/support/**/*'
32
+ - 'spec/rubocop/cop/vicenzo/rspec/leaky_definition_spec.rb'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2025-11-27
4
+
5
+ - Remove RuboCop::Cop::Vicenzo::RSpec::MixedExampleGroups in favor of InconsistentSiblingStructure #10;
6
+
7
+ - Add RoboCop::Cop::Vicenzo::RSpec::LeakyDefinition #9;
8
+ - Add RoboCop::Cop::Vicenzo::RSpec::InconsistentSiblingStructure #10;
9
+
10
+ - Fix NestedContextImproperStart to deal with all nested contexts #10;
11
+ - Fix NestedLetRedefinition to not point sibling lets as nested #10;
12
+ - Fix NestedSubjectRedefinition to not point sibling lets as nested #10;
13
+
14
+
15
+ ## [0.1.1] - 2025-08-12
16
+
17
+ - Add Rightly enable all cops #7;
18
+ - Fix RuboCop::Cop::Vicenzo::Rails::EnumInclusionOfValidation working with array format and no options #7;
19
+
3
20
  ## [0.1.0] - 2025-04-02
4
21
 
5
22
  - Initial release;
data/config/default.yml CHANGED
@@ -1,11 +1,19 @@
1
- Vicenzo/RSpec/MixedExampleGroups:
2
- Description: 'Check if there are example and groups at same level'
3
- Enabled: warning
1
+ Vicenzo/Rails/EnumInclusionOfValidation:
2
+ Description: 'Check if the enum has the inclusion of validation defined.'
3
+ Enabled: true
4
+ Severity: convention
4
5
  VersionAdded: '0.1.0'
5
6
 
7
+ Vicenzo/RSpec/InconsistentSiblingStructure:
8
+ Description: 'Enforces strict structural consistency (e.g. prevents mixing describe with context or examples with groups).'
9
+ Enabled: true
10
+ Severity: warning
11
+ VersionAdded: '0.2.0'
12
+
6
13
  Vicenzo/RSpec/NestedContextImproperStart:
7
14
  Description: 'Check if the nested context does not start as a root one.'
8
- Enabled: warning
15
+ Enabled: true
16
+ Severity: convention
9
17
  VersionAdded: '0.1.0'
10
18
 
11
19
  Vicenzo/RSpec/NestedLetRedefinition:
@@ -16,10 +24,17 @@ Vicenzo/RSpec/NestedLetRedefinition:
16
24
 
17
25
  Vicenzo/RSpec/NestedSubjectRedefinition:
18
26
  Description: 'Check if a subject is redefined in a nested example group.'
19
- Enabled: warning
27
+ Enabled: true
28
+ Severity: warning
20
29
  VersionAdded: '0.1.0'
21
30
 
22
- Vicenzo/Rails/EnumInclusionOfValidation:
23
- Description: 'Check if the enum has the inclusion of validation defined.'
24
- Enabled: convention
25
- VersionAdded: '0.1.0'
31
+ Vicenzo/RSpec/LeakyDefinition:
32
+ Description: 'Do not define methods, classes, or modules directly in spec files (unless inside spec/support).'
33
+ Enabled: true
34
+ Severity: warning
35
+ VersionAdded: '0.2.0'
36
+ Include:
37
+ - '**/spec/**/*_spec.rb'
38
+ Exclude:
39
+ - '**/spec/support/**/*'
40
+ - '**/spec/factories/**/*'
@@ -52,7 +52,7 @@ module RuboCop
52
52
  private
53
53
 
54
54
  def find_validate_option(enum_node)
55
- enum_node.last_argument.each_pair.find { |pair| pair.key.value == :validate }
55
+ options_node_for(enum_node)&.each_pair&.find { |pair| pair.key.value == :validate }
56
56
  end
57
57
 
58
58
  def valid_validate_option?(validate_kwarg)
@@ -62,14 +62,15 @@ module RuboCop
62
62
  end
63
63
  end
64
64
 
65
- def last_hash_node(enum_node)
65
+ def options_node_for(enum_node)
66
66
  enum_node.last_argument if enum_node.last_argument.hash_type?
67
67
  end
68
68
 
69
69
  def register_offence_for(enum_node, validate_kwarg)
70
70
  if validate_kwarg.nil?
71
71
  add_offense(enum_node, message: MSG_MISSING_VALIDATE) do |corrector|
72
- corrector.insert_after(last_hash_node(enum_node), ', validate: { allow_nil: true }')
72
+ last_node = options_node_for(enum_node) || enum_node.last_argument
73
+ corrector.insert_after(last_node, ', validate: { allow_nil: true }')
73
74
  end
74
75
  elsif !valid_validate_option?(validate_kwarg)
75
76
  add_offense(validate_kwarg, message: MSG_INVALID_VALIDATE) do |corrector|
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vicenzo
6
+ module RSpec
7
+ # Enforces strict structural consistency in RSpec files.
8
+ #
9
+ # It forbids mixing:
10
+ # 1. Examples (`it`) with Groups (`describe` or `context`)
11
+ # 2. Different types of Groups (`describe` with `context`)
12
+ #
13
+ # @example
14
+ # # bad (Mixing Describe and Context)
15
+ # RSpec.describe User do
16
+ # describe '#admin?' do ... end
17
+ # context 'when user is logged' do ... end
18
+ # end
19
+ #
20
+ # # bad (Mixing Example and Context)
21
+ # RSpec.describe User do
22
+ # it { is_expected.to be_valid }
23
+ # context 'when invalid' do ... end
24
+ # end
25
+ #
26
+ # # good
27
+ # RSpec.describe User do
28
+ # describe '#admin?' do ... end
29
+ # describe '#client?' do ... end
30
+ # end
31
+ #
32
+ class InconsistentSiblingStructure < RuboCop::Cop::RSpec::Base
33
+ MSG = 'Do not mix %<type_a>s with %<type_b>s at the same level.'
34
+
35
+ EXAMPLES = %i[it specify example scenario focus].freeze
36
+ DESCRIBES = %i[describe feature experiment].freeze
37
+ CONTEXTS = %i[context].freeze
38
+
39
+ # @!method example_definition?(node)
40
+ def_node_matcher :example_definition?, <<~PATTERN
41
+ (block (send nil? {#{EXAMPLES.map(&:inspect).join(' ')}} ...) ...)
42
+ PATTERN
43
+
44
+ # @!method describe_definition?(node)
45
+ def_node_matcher :describe_definition?, <<~PATTERN
46
+ (block (send nil? {#{DESCRIBES.map(&:inspect).join(' ')}} ...) ...)
47
+ PATTERN
48
+
49
+ # @!method context_definition?(node)
50
+ def_node_matcher :context_definition?, <<~PATTERN
51
+ (block (send nil? {#{CONTEXTS.map(&:inspect).join(' ')}} ...) ...)
52
+ PATTERN
53
+
54
+ def on_block(node)
55
+ return unless example_group?(node)
56
+ return unless node.body
57
+
58
+ # Normaliza e classifica em passos separados
59
+ children = child_nodes_for(node)
60
+ found_nodes = classify_children(children)
61
+
62
+ validate_consistency(found_nodes)
63
+ end
64
+
65
+ alias on_numblock on_block
66
+
67
+ private
68
+
69
+ def child_nodes_for(node)
70
+ node.body.begin_type? ? node.body.each_child_node : [node.body]
71
+ end
72
+
73
+ def classify_children(nodes)
74
+ classified = { example: [], describe: [], context: [] }
75
+
76
+ nodes.each do |child|
77
+ next unless child.block_type?
78
+
79
+ type = node_type(child)
80
+ classified[type] << child if type
81
+ end
82
+
83
+ classified
84
+ end
85
+
86
+ def node_type(node)
87
+ if example_definition?(node)
88
+ :example
89
+ elsif describe_definition?(node)
90
+ :describe
91
+ elsif context_definition?(node)
92
+ :context
93
+ end
94
+ end
95
+
96
+ def validate_consistency(nodes)
97
+ present_types = nodes.keys.select { |type| nodes[type].any? }
98
+
99
+ return if present_types.size <= 1
100
+
101
+ check_pair(nodes, :example, :describe)
102
+ check_pair(nodes, :example, :context)
103
+ check_pair(nodes, :describe, :context)
104
+ end
105
+
106
+ def check_pair(nodes, type_a, type_b)
107
+ return unless nodes[type_a].any? && nodes[type_b].any?
108
+
109
+ nodes[type_b].each do |node|
110
+ add_offense(node, message: format(MSG, type_a: type_a, type_b: type_b))
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Vicenzo
8
+ module RSpec
9
+ # Checks for methods, classes, or modules defined directly within
10
+ # RSpec blocks or at the top level of a spec file.
11
+ #
12
+ # Such definitions pollute the global namespace or the test class scope,
13
+ # leading to state leaking and intermittent test failures.
14
+ #
15
+ # @example
16
+ # # bad
17
+ # describe User do
18
+ # def setup_user
19
+ # # ...
20
+ # end
21
+ # end
22
+ #
23
+ # # bad
24
+ # def global_helper
25
+ # end
26
+ #
27
+ # # bad
28
+ # def stub_service
29
+ # allow(Service).to receive(:call)
30
+ # end
31
+ #
32
+ # # good
33
+ # describe User do
34
+ # let(:user) { create(:user) }
35
+ # end
36
+ #
37
+ # # good (inside anonymous class)
38
+ # let(:model) do
39
+ # Class.new do
40
+ # def safe_method; end
41
+ # end
42
+ # end
43
+ #
44
+ # # good
45
+ # before do
46
+ # allow(Service).to receive(:call)
47
+ # end
48
+ #
49
+ class LeakyDefinition < RuboCop::Cop::RSpec::Base
50
+ MSG = 'Do not define methods, classes, or modules directly in the global scope or within spec blocks. ' \
51
+ 'This pollutes the namespace. ' \
52
+ 'Move this logic to `spec/support`, use `let`, before, ' \
53
+ 'or encapsulate it within a safe structure (e.g., `Class.new`).'
54
+
55
+ # Matcher to identify dynamic class/module definitions commonly used in specs
56
+ # Looks for blocks applied to Class.new, Module.new, or Struct.new
57
+ # considers controller {} a valid mock
58
+ # @!method dynamic_definition?(node)
59
+ def_node_matcher :dynamic_definition?, <<~PATTERN
60
+ (block
61
+ {
62
+ (send (const {nil? cbase} {:Class :Module :Struct}) :new ...)
63
+ (send nil? :controller ...)
64
+ }
65
+ ...
66
+ )
67
+ PATTERN
68
+
69
+ def on_def(node)
70
+ check_node(node)
71
+ end
72
+
73
+ def on_class(node)
74
+ check_node(node)
75
+ end
76
+
77
+ def on_module(node)
78
+ check_node(node)
79
+ end
80
+
81
+ private
82
+
83
+ def check_node(node)
84
+ # If inside a safe scope (static or dynamic class/module), it's allowed.
85
+ return if inside_safe_scope?(node)
86
+
87
+ add_offense(node)
88
+ end
89
+
90
+ def inside_safe_scope?(node)
91
+ # Traverse ancestors to find a "protective shield"
92
+ node.each_ancestor.any? do |ancestor|
93
+ # 1. Is it a traditional definition? (class Foo; end)
94
+ next true if ancestor.type?(:class, :module)
95
+
96
+ # 2. Is it a dynamic definition? (Class.new do; end)
97
+ next true if dynamic_definition?(ancestor)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -26,32 +26,45 @@ module RuboCop
26
26
 
27
27
  FORBIDDEN_PREFIXES = %w[when with without].freeze
28
28
 
29
+ # @!method context_block?(node)
30
+ def_node_matcher :context_block?, <<~PATTERN
31
+ (block (send nil? :context ...) ...)
32
+ PATTERN
33
+
34
+ # @!method example_group_block?(node)
35
+ def_node_matcher :example_group_block?, <<~PATTERN
36
+ (block (send nil? {:describe :context :feature :example_group} ...) ...)
37
+ PATTERN
38
+
29
39
  def on_block(node)
30
- return unless context_block?(node) && context_block?(node.parent)
40
+ return unless context_block?(node)
31
41
 
32
- context_description = description_for(node)
33
- return unless context_description
42
+ parent = find_closest_example_group(node)
34
43
 
35
- first_word = context_description.split.first&.downcase
36
- return unless FORBIDDEN_PREFIXES.include?(first_word)
44
+ return unless parent && context_block?(parent)
37
45
 
38
- add_offense(node.send_node)
46
+ check_description(node)
39
47
  end
40
48
 
41
49
  alias on_numblock on_block
42
50
 
43
51
  private
44
52
 
45
- def context_block?(node)
46
- !node.nil? && node.block_type? && node.send_node.command?(:context)
53
+ def find_closest_example_group(node)
54
+ node.each_ancestor(:block).find { |ancestor| example_group_block?(ancestor) }
47
55
  end
48
56
 
49
- def description_for(context_node)
50
- description = context_node.send_node.first_argument
57
+ def check_description(node)
58
+ description_node = node.send_node.first_argument
51
59
 
52
- return if description.nil?
60
+ return unless description_node&.str_type?
53
61
 
54
- description.source.delete_prefix("'").delete_suffix("'")
62
+ text = description_node.value.to_s.strip
63
+ first_word = text.split.first&.downcase
64
+
65
+ return unless FORBIDDEN_PREFIXES.include?(first_word)
66
+
67
+ add_offense(node.send_node)
55
68
  end
56
69
  end
57
70
  end
@@ -90,11 +90,12 @@ module RuboCop
90
90
  end
91
91
 
92
92
  def check_let(let_node, let_definitions)
93
- name = (let_name(let_node) || let_it_be_name(let_node)).to_s.to_sym
93
+ name = (let_name(let_node) || let_it_be_name(let_node))&.to_s&.to_sym
94
94
 
95
95
  if let_definitions.key?(name)
96
96
  add_offense(let_node, message: redefined_let_message(name, let_definitions))
97
- let_definitions[name] << line_location(let_node)
97
+
98
+ let_definitions[name] += [line_location(let_node)]
98
99
  else
99
100
  let_definitions[name] = [line_location(let_node)]
100
101
  end
@@ -96,7 +96,7 @@ module RuboCop
96
96
 
97
97
  if subject_definitions.key?(name)
98
98
  add_offense(subject_node, message: redefined_subject_message(name, subject_definitions))
99
- subject_definitions[name] << line_location(subject_node)
99
+ subject_definitions[name] += [line_location(subject_node)]
100
100
  else
101
101
  subject_definitions[name] = [line_location(subject_node)]
102
102
  end
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'vicenzo/rspec/mixed_example_groups'
3
+ require_relative 'vicenzo/rspec/inconsistent_sibling_structure'
4
4
  require_relative 'vicenzo/rspec/nested_context_improper_start'
5
5
  require_relative 'vicenzo/rspec/nested_let_redefinition'
6
6
  require_relative 'vicenzo/rspec/nested_subject_redefinition'
7
+ require_relative 'vicenzo/rspec/leaky_definition'
7
8
  require_relative 'vicenzo/rails/enum_inclusion_of_validation'
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Vicenzo
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-vicenzo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bruno Vicenzo
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-02 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: lint_roller
@@ -51,7 +51,8 @@ dependencies:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
53
  version: 3.5.0
54
- description: Cops with good pratices I have been learning
54
+ description: A growing set of custom RuboCop cops capturing the best practices I've
55
+ been learning.
55
56
  email:
56
57
  - bruno@alumni.usp.br
57
58
  executables: []
@@ -68,7 +69,8 @@ files:
68
69
  - config/default.yml
69
70
  - lib/rubocop-vicenzo.rb
70
71
  - lib/rubocop/cop/vicenzo/rails/enum_inclusion_of_validation.rb
71
- - lib/rubocop/cop/vicenzo/rspec/mixed_example_groups.rb
72
+ - lib/rubocop/cop/vicenzo/rspec/inconsistent_sibling_structure.rb
73
+ - lib/rubocop/cop/vicenzo/rspec/leaky_definition.rb
72
74
  - lib/rubocop/cop/vicenzo/rspec/nested_context_improper_start.rb
73
75
  - lib/rubocop/cop/vicenzo/rspec/nested_let_redefinition.rb
74
76
  - lib/rubocop/cop/vicenzo/rspec/nested_subject_redefinition.rb
@@ -101,7 +103,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
101
103
  - !ruby/object:Gem::Version
102
104
  version: '0'
103
105
  requirements: []
104
- rubygems_version: 3.6.2
106
+ rubygems_version: 3.7.2
105
107
  specification_version: 4
106
108
  summary: Cops of Bruno Vicenzo
107
109
  test_files: []
@@ -1,65 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RuboCop
4
- module Cop
5
- module Vicenzo
6
- module RSpec
7
- # Ensures that examples (`it`, `specify`, `example`)
8
- # are not mixed with groups (`describe`, `context`) at the same level.
9
- #
10
- # @example
11
- # # bad
12
- # RSpec.describe User do
13
- # it { is_expected.to validate_presence_of(:name) }
14
- # describe '#admin?' do
15
- # it { expect(true).to eq(true) }
16
- # end
17
- # end
18
- #
19
- # # bad
20
- # RSpec.describe User do
21
- # describe '#admin?' do
22
- # it { expect(true).to eq(true) }
23
- # context 'when email starts with' do
24
- # end
25
- # end
26
- # end
27
- #
28
- # # good
29
- # RSpec.describe User do
30
- # describe '#admin?' do
31
- # context 'when email starts with' do
32
- # it { expect(true).to eq(true) }
33
- # end
34
- # end
35
- # end
36
- class MixedExampleGroups < RuboCop::Cop::RSpec::Base
37
- MSG = 'Do not mix examples (`it`, `specify`, `example`) with groups (`describe`, `context`) ' \
38
- 'at the same level.'
39
-
40
- def on_block(node)
41
- return unless example_or_group?(node)
42
-
43
- parent = node.parent
44
- return unless parent
45
-
46
- children = parent.children.select { |child| example_or_group?(child) }
47
- example_nodes, group_nodes = children.partition { |n| example?(n) }
48
-
49
- return if example_nodes.empty? || group_nodes.empty?
50
-
51
- add_offense(node)
52
- end
53
-
54
- alias on_numblock on_block
55
-
56
- private
57
-
58
- def example_or_group?(node)
59
- example?(node) || example_group?(node)
60
- end
61
- end
62
- end
63
- end
64
- end
65
- end