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 +4 -4
- data/.github/workflows/publish.yml +52 -0
- data/lib/rubocop/cop/deprecated/auto_time_definition.rb +24 -0
- data/lib/rubocop/cop/deprecated/browser_driver.rb +22 -0
- data/lib/rubocop/cop/deprecated/mongoid_needs.rb +80 -0
- data/lib/rubocop/cop/rails/model_association_missing_through.rb +99 -0
- data/lib/rubocop/cop/rails/model_association_sorting.rb +93 -6
- data/lib/rubocop/nueca/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0e3ce6ea4962d0409ef753cd4cfb316f81a8f40228539b62871d968b54df17f2
|
4
|
+
data.tar.gz: 34d40d5a28a407927510a815ce108121d2ab60bfc157ce2eac671af5b3a18c16
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
79
|
-
|
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
|
104
|
+
return if actual_order == expected_order
|
82
105
|
|
83
|
-
|
84
|
-
message = format(MSG, expected:
|
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
|
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.
|
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
|