rubocop-rspec-guide 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.
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpecGuide
6
+ # Detects examples that repeat in all leaf contexts.
7
+ # These invariants should be extracted to shared_examples.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # describe 'Validator' do
12
+ # context 'with valid data' do
13
+ # it 'responds to valid?' do
14
+ # expect(subject).to respond_to(:valid?)
15
+ # end
16
+ # end
17
+ #
18
+ # context 'with invalid data' do
19
+ # it 'responds to valid?' do
20
+ # expect(subject).to respond_to(:valid?)
21
+ # end
22
+ # end
23
+ #
24
+ # context 'with empty data' do
25
+ # it 'responds to valid?' do
26
+ # expect(subject).to respond_to(:valid?)
27
+ # end
28
+ # end
29
+ # end
30
+ #
31
+ # # good
32
+ # shared_examples 'a validator' do
33
+ # it 'responds to valid?' do
34
+ # expect(subject).to respond_to(:valid?)
35
+ # end
36
+ # end
37
+ #
38
+ # describe 'Validator' do
39
+ # context 'with valid data' do
40
+ # it_behaves_like 'a validator'
41
+ # end
42
+ #
43
+ # context 'with invalid data' do
44
+ # it_behaves_like 'a validator'
45
+ # end
46
+ #
47
+ # context 'with empty data' do
48
+ # it_behaves_like 'a validator'
49
+ # end
50
+ # end
51
+ #
52
+ class InvariantExamples < Base
53
+ MSG = "Example `%<description>s` repeats in all %<count>d leaf contexts. " \
54
+ "Consider extracting to shared_examples as an interface invariant."
55
+
56
+ # @!method example_with_description?(node)
57
+ def_node_matcher :example_with_description?, <<~PATTERN
58
+ (block
59
+ (send nil? {:it :specify :example} (str $_description))
60
+ ...)
61
+ PATTERN
62
+
63
+ # @!method context_or_describe?(node)
64
+ def_node_matcher :context_or_describe?, <<~PATTERN
65
+ (block
66
+ (send nil? {:describe :context} ...)
67
+ ...)
68
+ PATTERN
69
+
70
+ # @!method top_level_describe?(node)
71
+ def_node_matcher :top_level_describe?, <<~PATTERN
72
+ (block
73
+ (send nil? :describe ...)
74
+ ...)
75
+ PATTERN
76
+
77
+ def on_block(node)
78
+ return unless top_level_describe?(node)
79
+
80
+ # Find all leaf contexts (contexts with no nested contexts)
81
+ leaf_contexts = find_leaf_contexts(node)
82
+
83
+ min_leaf_contexts = cop_config["MinLeafContexts"] || 3
84
+ return if leaf_contexts.size < min_leaf_contexts
85
+
86
+ # Collect example descriptions from each leaf
87
+ examples_by_leaf = leaf_contexts.map do |leaf|
88
+ collect_example_descriptions(leaf)
89
+ end
90
+
91
+ # Find descriptions that appear in ALL leaves
92
+ common_descriptions = examples_by_leaf.reduce(:&)
93
+ return if common_descriptions.nil? || common_descriptions.empty?
94
+
95
+ # Add offenses for all examples with common descriptions
96
+ leaf_contexts.each do |leaf|
97
+ leaf.each_descendant(:block) do |example_node|
98
+ example_with_description?(example_node) do |description|
99
+ if common_descriptions.include?(description)
100
+ add_offense(
101
+ example_node,
102
+ message: format(MSG, description: description, count: leaf_contexts.size)
103
+ )
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ private
111
+
112
+ def find_leaf_contexts(node)
113
+ leaves = []
114
+
115
+ node.each_descendant(:block) do |child|
116
+ next unless context_or_describe?(child)
117
+
118
+ # Check if this context has nested contexts
119
+ has_nested = child.each_descendant(:block).any? do |nested|
120
+ context_or_describe?(nested) && nested != child
121
+ end
122
+
123
+ leaves << child unless has_nested
124
+ end
125
+
126
+ leaves
127
+ end
128
+
129
+ def collect_example_descriptions(context_node)
130
+ descriptions = []
131
+
132
+ context_node.each_descendant(:block) do |child|
133
+ example_with_description?(child) do |description|
134
+ descriptions << description
135
+ end
136
+ end
137
+
138
+ descriptions.uniq
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module RSpec
5
+ module Guide
6
+ VERSION = "0.2.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "guide/version"
4
+
5
+ module RuboCop
6
+ module RSpec
7
+ module Guide
8
+ class Error < StandardError; end
9
+ # Your code goes here...
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+ require "rubocop-rspec"
5
+
6
+ require_relative "rubocop/rspec/guide/version"
7
+ require_relative "rubocop/cop/rspec_guide/characteristics_and_contexts"
8
+ require_relative "rubocop/cop/rspec_guide/duplicate_let_values"
9
+ require_relative "rubocop/cop/rspec_guide/duplicate_before_hooks"
10
+ require_relative "rubocop/cop/rspec_guide/invariant_examples"
11
+ require_relative "rubocop/cop/rspec_guide/happy_path_first"
12
+ require_relative "rubocop/cop/rspec_guide/context_setup"
13
+ require_relative "rubocop/cop/factory_bot_guide/dynamic_attributes_for_time_and_random"
@@ -0,0 +1,8 @@
1
+ module RuboCop
2
+ module RSpec
3
+ module Guide
4
+ VERSION: String
5
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
6
+ end
7
+ end
8
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rubocop-rspec-guide
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Alexey Matskevich
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-01 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rubocop
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.50'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.50'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rubocop-rspec
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.20'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.20'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '13.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '13.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rspec
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3.12'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '3.12'
68
+ - !ruby/object:Gem::Dependency
69
+ name: standard
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.24'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.24'
82
+ description: A collection of custom RuboCop cops that enforce best practices from
83
+ the RSpec style guide, including context structure, testing patterns, and FactoryBot
84
+ usage.
85
+ email:
86
+ - github_job@mackevich.addymail.com
87
+ executables: []
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".rspec"
92
+ - ".rubocop.yml"
93
+ - ".standard.yml"
94
+ - CHANGELOG.md
95
+ - LICENSE.txt
96
+ - README.md
97
+ - Rakefile
98
+ - config/default.yml
99
+ - devbox.json
100
+ - devbox.lock
101
+ - lib/rubocop-rspec-guide.rb
102
+ - lib/rubocop/cop/factory_bot_guide/dynamic_attributes_for_time_and_random.rb
103
+ - lib/rubocop/cop/rspec_guide/characteristics_and_contexts.rb
104
+ - lib/rubocop/cop/rspec_guide/context_setup.rb
105
+ - lib/rubocop/cop/rspec_guide/duplicate_before_hooks.rb
106
+ - lib/rubocop/cop/rspec_guide/duplicate_let_values.rb
107
+ - lib/rubocop/cop/rspec_guide/happy_path_first.rb
108
+ - lib/rubocop/cop/rspec_guide/invariant_examples.rb
109
+ - lib/rubocop/rspec/guide.rb
110
+ - lib/rubocop/rspec/guide/version.rb
111
+ - sig/rubocop/rspec/guide.rbs
112
+ homepage: https://github.com/rspec-guide/rubocop-rspec-guide
113
+ licenses:
114
+ - MIT
115
+ metadata:
116
+ homepage_uri: https://github.com/rspec-guide/rubocop-rspec-guide
117
+ source_code_uri: https://github.com/rspec-guide/rubocop-rspec-guide
118
+ changelog_uri: https://github.com/rspec-guide/rubocop-rspec-guide/blob/master/CHANGELOG.md
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: 3.0.0
127
+ required_rubygems_version: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ requirements: []
133
+ rubygems_version: 3.7.1
134
+ specification_version: 4
135
+ summary: Custom RuboCop cops based on the RSpec best practices guide
136
+ test_files: []