rubocop-nueca 1.0.0 → 1.1.3

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: 4a58e87bfcf5ac18b1e87c51da6a3fd4b37e53c664ec86518ae05c5275b446b2
4
- data.tar.gz: 9f08cc06b6f03c0bd16ab76fd3b66e2540f2e4c1ba06d23a02f0bffe49c778fa
3
+ metadata.gz: 0e3ce6ea4962d0409ef753cd4cfb316f81a8f40228539b62871d968b54df17f2
4
+ data.tar.gz: 34d40d5a28a407927510a815ce108121d2ab60bfc157ce2eac671af5b3a18c16
5
5
  SHA512:
6
- metadata.gz: 436c241836f68bd0136f66d6639ea08c2d8e935583609a421c1045222903aff380a4553df6169dd9131e7e6fddeaf82110a003e4050f0966c1955bbcaa8d3d33
7
- data.tar.gz: 27fb4e271c37e2ee980669e81f8c0237d3314a3fc77ba0310bc2401c75430b99cbe765991c418b848432c3ff20de036e338c09a86d26370171368bcac46aedae
6
+ metadata.gz: 10f24a5fd49e0ee7344e4a260fb58af10ff80a6711a828ff76dfc204d7f17177ed79757fcea03488efba7cc46051c9bcb9c2bdf297ddef3207367340caebafab
7
+ data.tar.gz: c446cf64ec058628f6c7540ee9519844b83e81d28db772524d86438a96d1343407a7a8af5ef8f6172f5cd5e6afe200c66b7089433114af45ed36931625b7dbea
@@ -0,0 +1,52 @@
1
+ name: Publish Gem
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+
14
+ - name: Set up Ruby
15
+ uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: '3.4'
18
+ bundler-cache: true
19
+
20
+ - name: Get gem version
21
+ id: gem_version
22
+ run: |
23
+ VERSION=$(ruby -e "require './lib/rubocop/nueca/version'; puts RuboCop::Nueca::VERSION")
24
+ echo "version=$VERSION" >> $GITHUB_OUTPUT
25
+
26
+ - name: Check if version exists on RubyGems
27
+ id: version_check
28
+ run: |
29
+ if gem list -r rubocop-nueca | grep -q "${{ steps.gem_version.outputs.version }}"; then
30
+ echo "exists=true" >> $GITHUB_OUTPUT
31
+ else
32
+ echo "exists=false" >> $GITHUB_OUTPUT
33
+ fi
34
+
35
+ - name: Build gem
36
+ if: steps.version_check.outputs.exists == 'false'
37
+ run: gem build *.gemspec
38
+
39
+ - name: Publish to RubyGems
40
+ if: steps.version_check.outputs.exists == 'false'
41
+ run: |
42
+ mkdir -p $HOME/.gem
43
+ touch $HOME/.gem/credentials
44
+ chmod 0600 $HOME/.gem/credentials
45
+ printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
46
+ gem push *.gem
47
+ env:
48
+ GEM_HOST_API_KEY: ${{ secrets.RUBYGEMS_AUTH_TOKEN }}
49
+
50
+ - name: Skip publishing
51
+ if: steps.version_check.outputs.exists == 'true'
52
+ run: echo "Version ${{ steps.gem_version.outputs.version }} already exists on RubyGems. Skipping publish."
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ class AutoTimeDefinition < RuboCop::Cop::Base
7
+ MSG = 'Use autotime(...) helper instead of using Date, DateTime or Time.'
8
+ RESTRICT_ON_SEND = [:new, :today, :now].freeze
9
+
10
+ def_node_matcher :datetime_usage, <<~PATTERN
11
+ (send
12
+ (const nil? {:DateTime | :Time | :Date}) ...)
13
+ PATTERN
14
+
15
+ def on_send(node)
16
+ return unless node.source_range.source_buffer.name.include?('_spec.rb')
17
+ return unless datetime_usage(node)
18
+
19
+ add_offense(node)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Capybara
6
+ class BrowserDriver < ::RuboCop::Cop::Base
7
+ MSG = 'Use default driver instead of :browser.'
8
+
9
+ def_node_matcher :browser_usage, <<~PATTERN
10
+ (send _ _ _ (sym :browser) ...)
11
+ PATTERN
12
+
13
+ def on_send(node)
14
+ return unless node.source_range.source_buffer.name.include?('_spec.rb')
15
+ return unless browser_usage(node)
16
+
17
+ add_offense(node)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ class MongoidNeeds < RuboCop::Cop::Base
7
+ extend RuboCop::Cop::AutoCorrector
8
+
9
+ SAFETY = :unsafe
10
+ MSG = 'Add `needs: :mongoid` to RSpec.describe when requiring mongoid_helper.'
11
+
12
+ def_node_matcher :require_mongoid_helper, <<~PATTERN
13
+ (send nil? :require (str "mongoid_helper"))
14
+ PATTERN
15
+
16
+ def_node_matcher :rspec_describe_with_needs, <<~PATTERN
17
+ (send
18
+ (const nil? :RSpec) :describe
19
+ _
20
+ (hash <(pair (sym :needs) (sym :mongoid)) ...>))
21
+ PATTERN
22
+
23
+ def_node_matcher :rspec_describe_without_needs, <<~PATTERN
24
+ (send
25
+ (const nil? :RSpec) :describe
26
+ _
27
+ $...)
28
+ PATTERN
29
+
30
+ def on_send(node)
31
+ buffer = node.source_range.source_buffer
32
+ return unless buffer.name.include?('_spec.rb')
33
+ return unless require_mongoid_helper(node)
34
+
35
+ root_node = processed_source.ast
36
+ describe_node = find_rspec_describe(root_node)
37
+
38
+ return unless describe_node
39
+ return if rspec_describe_with_needs(describe_node)
40
+
41
+ add_offense(describe_node, message: MSG) do |corrector|
42
+ autocorrect(corrector, describe_node)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def find_rspec_describe(node)
49
+ return node if rspec_describe_without_needs(node)
50
+
51
+ node.children.each do |child|
52
+ next unless child.is_a?(Parser::AST::Node)
53
+
54
+ result = find_rspec_describe(child)
55
+ return result if result
56
+ end
57
+
58
+ nil
59
+ end
60
+
61
+ def autocorrect(corrector, node)
62
+ args = node.arguments
63
+ last_arg = args.last
64
+ if args.size == 2 && last_arg.hash_type?
65
+ hash_node = last_arg
66
+ last_pair = hash_node.pairs.last
67
+
68
+ if last_pair
69
+ corrector.insert_after(last_pair, ', needs: :mongoid')
70
+ else
71
+ corrector.insert_after(hash_node.loc.begin, 'needs: :mongoid')
72
+ end
73
+ else
74
+ corrector.insert_after(last_arg, ', needs: :mongoid')
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ class ModelAssociationMissingThrough < RuboCop::Cop::Base
7
+ MSG = 'Association %<association>s references through %<through>s, ' \
8
+ 'but %<through>s is not defined in this model.'
9
+ ASSOCIATION_METHODS = [
10
+ :has_one,
11
+ :has_many,
12
+ :has_and_belongs_to_many
13
+ ].freeze
14
+
15
+ def on_class(node)
16
+ return unless model_class?(node)
17
+
18
+ associations = find_associations(node)
19
+ return if associations.empty?
20
+
21
+ check_missing_through_associations(associations)
22
+ end
23
+
24
+ private
25
+
26
+ def model_class?(node)
27
+ parent_class = node.parent_class
28
+ return false unless parent_class
29
+
30
+ parent_name = parent_class.const_name
31
+ ['ApplicationRecord', 'ActiveRecord::Base'].include?(parent_name)
32
+ end
33
+
34
+ def find_associations(class_node)
35
+ associations = []
36
+
37
+ class_node.body&.each_child_node do |child|
38
+ next unless child.type == :send
39
+ next unless association_method?(child)
40
+
41
+ associations << {
42
+ node: child,
43
+ method: child.method_name,
44
+ name: association_name(child),
45
+ through: association_through(child)
46
+ }
47
+ end
48
+
49
+ associations
50
+ end
51
+
52
+ def association_method?(node)
53
+ return false unless node.receiver.nil?
54
+
55
+ ASSOCIATION_METHODS.include?(node.method_name)
56
+ end
57
+
58
+ def association_name(node)
59
+ first_arg = node.arguments.first
60
+ return nil unless first_arg&.sym_type?
61
+
62
+ first_arg.value.to_s
63
+ end
64
+
65
+ def association_through(node)
66
+ options_hash = find_options_hash(node)
67
+ return nil unless options_hash
68
+
69
+ through_pair = options_hash.pairs.find do |pair|
70
+ pair.key.sym_type? && pair.key.value == :through
71
+ end
72
+
73
+ return nil unless through_pair&.value&.sym_type?
74
+
75
+ through_pair.value.value.to_s
76
+ end
77
+
78
+ def find_options_hash(node)
79
+ node.arguments.find(&:hash_type?)
80
+ end
81
+
82
+ def check_missing_through_associations(associations)
83
+ defined_associations = associations.to_set { |assoc| assoc[:name] }
84
+
85
+ associations.each do |assoc|
86
+ next unless assoc[:through]
87
+
88
+ next if defined_associations.include?(assoc[:through])
89
+
90
+ message = format(MSG,
91
+ association: assoc[:name],
92
+ through: assoc[:through])
93
+ add_offense(assoc[:node], message: message)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Rails
6
- class ModelAssociationSorting < RuboCop::Cop::Base
6
+ class ModelAssociationSorting < RuboCop::Cop::Base # rubocop:disable Metrics/ClassLength
7
7
  MSG = 'Sort associations of the same type alphabetically. Expected order: %<expected>s.'
8
8
  ASSOCIATION_METHODS = [
9
9
  :belongs_to,
@@ -43,6 +43,7 @@ module RuboCop
43
43
  node: child,
44
44
  method: child.method_name,
45
45
  name: association_name(child),
46
+ through: association_through(child),
46
47
  start_line: source_range.line,
47
48
  end_line: source_range.last_line
48
49
  }
@@ -64,6 +65,23 @@ module RuboCop
64
65
  first_arg.value.to_s
65
66
  end
66
67
 
68
+ def association_through(node)
69
+ options_hash = find_options_hash(node)
70
+ return nil unless options_hash
71
+
72
+ through_pair = options_hash.pairs.find do |pair|
73
+ pair.key.sym_type? && pair.key.value == :through
74
+ end
75
+
76
+ return nil unless through_pair&.value&.sym_type?
77
+
78
+ through_pair.value.value.to_s
79
+ end
80
+
81
+ def find_options_hash(node)
82
+ node.arguments.find(&:hash_type?)
83
+ end
84
+
67
85
  def check_association_sorting(associations)
68
86
  grouped = associations.group_by { |assoc| assoc[:method] }
69
87
 
@@ -75,16 +93,85 @@ module RuboCop
75
93
  end
76
94
 
77
95
  def check_group_sorting(group_associations)
78
- names = group_associations.map { |assoc| assoc[:name] }
79
- sorted_names = names.sort
96
+ through_associations = group_associations.select { |assoc| assoc[:through] }
97
+ regular_associations = group_associations.reject { |assoc| assoc[:through] }
98
+
99
+ regular_sorted = regular_associations.sort_by { |assoc| assoc[:name] }
100
+
101
+ expected_order = build_expected_order(regular_sorted, through_associations)
102
+ actual_order = group_associations.map { |assoc| assoc[:name] }
80
103
 
81
- return if names == sorted_names
104
+ return if actual_order == expected_order
82
105
 
83
- expected_order = sorted_names.join(', ')
84
- message = format(MSG, expected: expected_order)
106
+ expected_order_str = expected_order.join(', ')
107
+ message = format(MSG, expected: expected_order_str)
85
108
 
86
109
  add_offense(group_associations.first[:node], message: message)
87
110
  end
111
+
112
+ def build_expected_order(regular_associations, through_associations)
113
+ all_associations = regular_associations + through_associations
114
+ association_lookup = build_association_lookup(all_associations)
115
+ dependency_graph = build_dependency_graph(all_associations, association_lookup)
116
+
117
+ topological_sort_alphabetically(dependency_graph, all_associations.map { |a| a[:name] })
118
+ end
119
+
120
+ def build_association_lookup(associations)
121
+ associations.index_by { |assoc| assoc[:name] }
122
+ end
123
+
124
+ def build_dependency_graph(associations, lookup)
125
+ graph = associations.each_with_object({}) { |assoc, deps| deps[assoc[:name]] = [] }
126
+
127
+ associations.each do |assoc|
128
+ graph[assoc[:through]] << assoc[:name] if assoc[:through] && lookup[assoc[:through]]
129
+ end
130
+
131
+ graph
132
+ end
133
+
134
+ def topological_sort_alphabetically(dependency_graph, all_nodes)
135
+ in_degrees = calculate_in_degrees(dependency_graph, all_nodes)
136
+ available_nodes = nodes_with_zero_dependencies(in_degrees)
137
+ result = []
138
+
139
+ until available_nodes.empty?
140
+ current = available_nodes.shift
141
+ result << current
142
+
143
+ process_dependents(current, dependency_graph, in_degrees, available_nodes)
144
+ end
145
+
146
+ result
147
+ end
148
+
149
+ def calculate_in_degrees(dependency_graph, all_nodes)
150
+ in_degrees = all_nodes.each_with_object({}) { |node, degrees| degrees[node] = 0 }
151
+
152
+ dependency_graph.each_value do |dependents|
153
+ dependents.each { |dependent| in_degrees[dependent] += 1 }
154
+ end
155
+
156
+ in_degrees
157
+ end
158
+
159
+ def nodes_with_zero_dependencies(in_degrees)
160
+ in_degrees.select { |_node, degree| degree.zero? }.keys.sort
161
+ end
162
+
163
+ def process_dependents(current_node, dependency_graph, in_degrees, available_nodes)
164
+ dependency_graph[current_node]&.each do |dependent|
165
+ in_degrees[dependent] -= 1
166
+
167
+ insert_alphabetically(available_nodes, dependent) if in_degrees[dependent].zero?
168
+ end
169
+ end
170
+
171
+ def insert_alphabetically(sorted_array, element)
172
+ insert_position = sorted_array.bsearch_index { |x| x > element } || sorted_array.length
173
+ sorted_array.insert(insert_position, element)
174
+ end
88
175
  end
89
176
  end
90
177
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Nueca
5
- VERSION = '1.0.0'
5
+ VERSION = '1.1.3'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-nueca
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tien
@@ -133,16 +133,21 @@ executables: []
133
133
  extensions: []
134
134
  extra_rdoc_files: []
135
135
  files:
136
+ - ".github/workflows/publish.yml"
136
137
  - CODE_OF_CONDUCT.md
137
138
  - LICENSE.txt
138
139
  - README.md
139
140
  - config/default.yml
140
141
  - lib/rubocop-nueca.rb
142
+ - lib/rubocop/cop/deprecated/auto_time_definition.rb
143
+ - lib/rubocop/cop/deprecated/browser_driver.rb
144
+ - lib/rubocop/cop/deprecated/mongoid_needs.rb
141
145
  - lib/rubocop/cop/rails/date_time_conversion.rb
142
146
  - lib/rubocop/cop/rails/date_time_current.rb
143
147
  - lib/rubocop/cop/rails/migration_table_variable.rb
144
148
  - lib/rubocop/cop/rails/model_association_consistent_spacing.rb
145
149
  - lib/rubocop/cop/rails/model_association_grouping.rb
150
+ - lib/rubocop/cop/rails/model_association_missing_through.rb
146
151
  - lib/rubocop/cop/rails/model_association_scattering.rb
147
152
  - lib/rubocop/cop/rails/model_association_separation.rb
148
153
  - lib/rubocop/cop/rails/model_association_sorting.rb