momocop 0.1.5 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f8ebe27766e45fdc287845e8dc4d8015b13b279a34ba58a0b85f3daa0406dd6
4
- data.tar.gz: c991e08f8256f641657868520293b117b694d7177142d8d9e82c226e18f85305
3
+ metadata.gz: 2217b77f50980c7b628b6be000f20a76093b41f13933b5e0e4c04c860215eb68
4
+ data.tar.gz: 3fd67a7eb04625b7021db01f453477f804b8e48609c9f41e859579d46239d89e
5
5
  SHA512:
6
- metadata.gz: d44b953c25c50ca2da56043dceb78b41db565f5226d84bf091ca1cc32bb886d08b93791751c511bc37eda7d1f509b27645f1ce4202e7c5e7b47656f53e963995
7
- data.tar.gz: '0348eb2e208e147158a837934efadeb796bbc0d4169a2c9dfb050eba282488ea6c28e96ed57d2deae4dd6202a996487162ecaad06a94faa537693b7b49edf49d'
6
+ metadata.gz: 5d5dfa8b78691d8cf4bc8938d3bed507969eec0dbc3940032a22eb3ca0fa705abbecc82f1f4586be8e8d9e963afd5144572399c2854abed205f85c866b75fbe4
7
+ data.tar.gz: 7feb8e1104912f6d595becbd4b4fa20ec3fd38910892d52da3f64b12f5fc6ac37f4ef352f6a57b29b094d5c2fce434895eb9c54d8bc0e5516c5ce24b9ce9a082
data/.rubocop.yml CHANGED
@@ -56,6 +56,9 @@ Style/FrozenStringLiteralComment:
56
56
  Style/IfUnlessModifier:
57
57
  Enabled: false
58
58
 
59
+ Style/ParallelAssignment:
60
+ Enabled: false
61
+
59
62
  Style/RequireOrder:
60
63
  Enabled: true
61
64
  SafeAutoCorrect: true
data/README.ja.md ADDED
@@ -0,0 +1,53 @@
1
+ [English](./README.md) | 日本語
2
+
3
+ # momocop
4
+
5
+ [![Spec](https://github.com/supermomonga/momocop/actions/workflows/spec.yml/badge.svg)](https://github.com/supermomonga/momocop/actions/workflows/spec.yml) [![Gem Version](https://badge.fury.io/rb/momocop.svg)](https://badge.fury.io/rb/momocop)
6
+
7
+ Momocopは、規約ベースのOpinionatedな[RuboCop](https://github.com/rubocop/rubocop)カスタムCopを提供します。
8
+
9
+ ## インストール
10
+
11
+ `momocop`をGemfileに追加してください。
12
+
13
+ ```rb
14
+ # Gemfile
15
+ gem 'momocop', require: false
16
+ ```
17
+
18
+ ## 使用方法
19
+
20
+ `.rubocop.yml`を編集して、`momocop`をrequireしてください。
21
+
22
+ すべてのCopはデフォルトで無効になっているため、利用したいCopを有効にしてください。
23
+
24
+ ```yaml
25
+ # .rubocop.yaml
26
+ require:
27
+ - momocop
28
+
29
+ Momocop/FactoryBotMissingAssociations:
30
+ Enabled: true
31
+ ```
32
+
33
+ ## Cop一覧
34
+
35
+ |Cop|Rails|FactoryBot|
36
+ |---|:-:|:-:|
37
+ |[`Momocop/FactoryBotClassExistence`](lib/rubocop/cop/momocop/factory_bot_class_existence.rb)|:white_check_mark:|:white_check_mark:|
38
+ |[`Momocop/FactoryBotMissingAssociations`](lib/rubocop/cop/momocop/factory_bot_missing_associations.rb)|:white_check_mark:|:white_check_mark:|
39
+ |[`Momocop/FactoryBotMissingClassOption`](lib/rubocop/cop/momocop/factory_bot_missing_class_option.rb)|:white_check_mark:|:white_check_mark:|
40
+ |[`Momocop/FactoryBotMissingProperties`](lib/rubocop/cop/momocop/factory_bot_missing_properties.rb)|:white_check_mark:|:white_check_mark:|
41
+ |[`Momocop/FactoryBotPropertyOrder`](lib/rubocop/cop/momocop/factory_bot_property_order.rb)|:white_check_mark:|:white_check_mark:|
42
+
43
+ ## 貢献
44
+
45
+ GitHubの https://github.com/supermomonga/momocop でのバグ報告やプルリクエストは歓迎します。このプロジェクトは安全で、協力的な空間であることを目指しており、貢献者は[行動規範](https://github.com/supermomonga/momocop/blob/main/CODE_OF_CONDUCT.md)に従うことが求められます。
46
+
47
+ ## ライセンス
48
+
49
+ このgemは[MITライセンス](https://opensource.org/licenses/MIT)の条件の下でオープンソースとして利用可能です。
50
+
51
+ ## 行動規範
52
+
53
+ Momocopプロジェクトのコードベース、イシュートラッカー、チャットルーム、およびメーリングリストにおいて交流する全員は、[行動規範](https://github.com/supermomonga/momocop/blob/main/CODE_OF_CONDUCT.md)に従うことが期待されます。
data/README.ja.md.erb ADDED
@@ -0,0 +1,62 @@
1
+ [English](./README.md) | 日本語
2
+
3
+ # momocop
4
+
5
+ [![Spec](https://github.com/supermomonga/momocop/actions/workflows/spec.yml/badge.svg)](https://github.com/supermomonga/momocop/actions/workflows/spec.yml) [![Gem Version](https://badge.fury.io/rb/momocop.svg)](https://badge.fury.io/rb/momocop)
6
+
7
+ Momocopは、規約ベースのOpinionatedな[RuboCop](https://github.com/rubocop/rubocop)カスタムCopを提供します。
8
+
9
+ ## インストール
10
+
11
+ `momocop`をGemfileに追加してください。
12
+
13
+ ```rb
14
+ # Gemfile
15
+ gem 'momocop', require: false
16
+ ```
17
+
18
+ ## 使用方法
19
+
20
+ `.rubocop.yml`を編集して、`momocop`をrequireしてください。
21
+
22
+ すべてのCopはデフォルトで無効になっているため、利用したいCopを有効にしてください。
23
+
24
+ ```yaml
25
+ # .rubocop.yaml
26
+ require:
27
+ - momocop
28
+
29
+ Momocop/FactoryBotMissingAssociations:
30
+ Enabled: true
31
+ ```
32
+
33
+ ## Cop一覧
34
+
35
+ |Cop|Rails|FactoryBot|
36
+ |---|:-:|:-:|
37
+ <%
38
+ files = Dir.glob('lib/rubocop/cop/momocop/*.rb').sort_by { |f| File.basename(f)}
39
+ files.each do |cop_file_path|
40
+ base_name = File.basename(cop_file_path, '.rb')
41
+ cop_class_name = "Momocop/#{base_name.camelize}"
42
+ is_factory_cop = cop_file_path.include?('factory_bot_')
43
+ is_rails_cop = cop_file_path.match?(/(factory_bot_|rails_)/)
44
+ rails_mark = is_rails_cop ? ':white_check_mark:' : ''
45
+ factory_bot_mark = is_factory_cop ? ':white_check_mark:' : ''
46
+
47
+ table_row = "|[`#{cop_class_name}`](#{cop_file_path})|#{rails_mark}|#{factory_bot_mark}|"
48
+ -%>
49
+ <%= table_row %>
50
+ <% end -%>
51
+
52
+ ## 貢献
53
+
54
+ GitHubの https://github.com/supermomonga/momocop でのバグ報告やプルリクエストは歓迎します。このプロジェクトは安全で、協力的な空間であることを目指しており、貢献者は[行動規範](https://github.com/supermomonga/momocop/blob/main/CODE_OF_CONDUCT.md)に従うことが求められます。
55
+
56
+ ## ライセンス
57
+
58
+ このgemは[MITライセンス](https://opensource.org/licenses/MIT)の条件の下でオープンソースとして利用可能です。
59
+
60
+ ## 行動規範
61
+
62
+ Momocopプロジェクトのコードベース、イシュートラッカー、チャットルーム、およびメーリングリストにおいて交流する全員は、[行動規範](https://github.com/supermomonga/momocop/blob/main/CODE_OF_CONDUCT.md)に従うことが期待されます。
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ English | [日本語](./README.ja.md)
2
+
1
3
  # momocop
2
4
 
3
5
  [![Spec](https://github.com/supermomonga/momocop/actions/workflows/spec.yml/badge.svg)](https://github.com/supermomonga/momocop/actions/workflows/spec.yml) [![Gem Version](https://badge.fury.io/rb/momocop.svg)](https://badge.fury.io/rb/momocop)
@@ -24,7 +26,7 @@ All cops are disabled by default.
24
26
  require:
25
27
  - momocop
26
28
 
27
- Momocop/FactoryBotRailsFactoryAssociationsCoverage:
29
+ Momocop/FactoryBotMissingAssociations:
28
30
  Enabled: true
29
31
  ```
30
32
 
@@ -32,10 +34,11 @@ Momocop/FactoryBotRailsFactoryAssociationsCoverage:
32
34
 
33
35
  |Cop|Rails|FactoryBot|
34
36
  |---|:-:|:-:|
35
- |[`Momocop/FactoryBotRailsClassName`](lib/rubocop/cop/momocop/factory_bot_rails_class_name.rb)|:white_check_mark:|:white_check_mark:|
36
- |[`Momocop/FactoryBotRailsClassOptionSpecified`](lib/rubocop/cop/momocop/factory_bot_rails_class_option_specified.rb)|:white_check_mark:|:white_check_mark:|
37
- |[`Momocop/FactoryBotRailsFactoryAssociationsCoverage`](lib/rubocop/cop/momocop/factory_bot_rails_factory_associations_coverage.rb)|:white_check_mark:|:white_check_mark:|
38
- |[`Momocop/FactoryBotRailsFactoryPropertiesCoverage`](lib/rubocop/cop/momocop/factory_bot_rails_factory_properties_coverage.rb)|:white_check_mark:|:white_check_mark:|
37
+ |[`Momocop/FactoryBotClassExistence`](lib/rubocop/cop/momocop/factory_bot_class_existence.rb)|:white_check_mark:|:white_check_mark:|
38
+ |[`Momocop/FactoryBotMissingAssociations`](lib/rubocop/cop/momocop/factory_bot_missing_associations.rb)|:white_check_mark:|:white_check_mark:|
39
+ |[`Momocop/FactoryBotMissingClassOption`](lib/rubocop/cop/momocop/factory_bot_missing_class_option.rb)|:white_check_mark:|:white_check_mark:|
40
+ |[`Momocop/FactoryBotMissingProperties`](lib/rubocop/cop/momocop/factory_bot_missing_properties.rb)|:white_check_mark:|:white_check_mark:|
41
+ |[`Momocop/FactoryBotPropertyOrder`](lib/rubocop/cop/momocop/factory_bot_property_order.rb)|:white_check_mark:|:white_check_mark:|
39
42
 
40
43
  ## Contributing
41
44
 
data/README.md.erb ADDED
@@ -0,0 +1,62 @@
1
+ English | [日本語](./README.ja.md)
2
+
3
+ # momocop
4
+
5
+ [![Spec](https://github.com/supermomonga/momocop/actions/workflows/spec.yml/badge.svg)](https://github.com/supermomonga/momocop/actions/workflows/spec.yml) [![Gem Version](https://badge.fury.io/rb/momocop.svg)](https://badge.fury.io/rb/momocop)
6
+
7
+ Momocop is highly opinionated custom cops for [RuboCop](https://github.com/rubocop/rubocop).
8
+
9
+ ## Installation
10
+
11
+ Add `momocop` to your Gemfile.
12
+
13
+ ```rb
14
+ # Gemfile
15
+ gem 'momocop', require: false
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ Edit `.rubocop.yml` to require `momocop` and enable the cops you want.
21
+
22
+ All cops are disabled by default.
23
+
24
+ ```yaml
25
+ # .rubocop.yaml
26
+ require:
27
+ - momocop
28
+
29
+ Momocop/FactoryBotMissingAssociations:
30
+ Enabled: true
31
+ ```
32
+
33
+ ## Cops
34
+
35
+ |Cop|Rails|FactoryBot|
36
+ |---|:-:|:-:|
37
+ <%
38
+ files = Dir.glob('lib/rubocop/cop/momocop/*.rb').sort_by { |f| File.basename(f)}
39
+ files.each do |cop_file_path|
40
+ base_name = File.basename(cop_file_path, '.rb')
41
+ cop_class_name = "Momocop/#{base_name.camelize}"
42
+ is_factory_cop = cop_file_path.include?('factory_bot_')
43
+ is_rails_cop = cop_file_path.match?(/(factory_bot_|rails_)/)
44
+ rails_mark = is_rails_cop ? ':white_check_mark:' : ''
45
+ factory_bot_mark = is_factory_cop ? ':white_check_mark:' : ''
46
+
47
+ table_row = "|[`#{cop_class_name}`](#{cop_file_path})|#{rails_mark}|#{factory_bot_mark}|"
48
+ -%>
49
+ <%= table_row %>
50
+ <% end -%>
51
+
52
+ ## Contributing
53
+
54
+ Bug reports and pull requests are welcome on GitHub at https://github.com/supermomonga/momocop. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/supermomonga/momocop/blob/main/CODE_OF_CONDUCT.md).
55
+
56
+ ## License
57
+
58
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
59
+
60
+ ## Code of Conduct
61
+
62
+ Everyone interacting in the Momocop project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/supermomonga/momocop/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -10,3 +10,24 @@ require 'rubocop/rake_task'
10
10
  RuboCop::RakeTask.new
11
11
 
12
12
  task default: %i[spec rubocop]
13
+
14
+ require 'active_support/core_ext/string/inflections'
15
+ require 'erb'
16
+
17
+ desc 'Update README.md from README.md.erb'
18
+ task 'readme:update' do
19
+ files = Dir.glob(File.join(__dir__, 'README.*.erb'))
20
+ files.each do |src_path|
21
+ dst_path = src_path.sub(/\.erb\z/, '')
22
+ # ERBテンプレートの読み込み
23
+ template = File.read(src_path)
24
+
25
+ # ERBテンプレートのレンダリング
26
+ renderer = ERB.new(template, trim_mode: '-')
27
+ output = renderer.result
28
+
29
+ # README.mdへの書き込み
30
+ File.write(dst_path, output)
31
+ puts "#{dst_path} has been successfully updated."
32
+ end
33
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Momocop
4
- VERSION = '0.1.5'
4
+ VERSION = '0.1.7'
5
5
  end
data/lib/momocop.rb CHANGED
@@ -8,6 +8,9 @@ require 'rubocop/cop/mixin/active_record_helper'
8
8
  require 'rubocop/rails/schema_loader'
9
9
  require 'rubocop/rails/schema_loader/schema'
10
10
 
11
+ # sevencop
12
+ require 'sevencop/cop_concerns'
13
+
11
14
  # Momocop
12
15
  require_relative 'momocop/association_extractor'
13
16
  require_relative 'momocop/config_injector'
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Momocop
6
+ # Ensures that FactoryBot factories has a valid class option.
7
+ #
8
+ # @example
9
+ # # bad (if 'app/models/user.rb' does not exist)
10
+ # factory :user, class: 'User' do
11
+ # end
12
+ #
13
+ # # good (if 'app/models/admin.rb' exists)
14
+ # factory :user, class: 'Admin' do
15
+ # end
16
+ class FactoryBotClassExistence < RuboCop::Cop::Base
17
+ extend AutoCorrector
18
+
19
+ MSG = 'Specified class does not exist. Please make sure that the class exists.'
20
+
21
+ RESTRICT_ON_SEND = %i[factory].freeze
22
+
23
+ def_node_matcher :factory_class_option_symbol?, <<~PATTERN
24
+ (send nil? :factory _ (hash <(pair (sym :class) $(sym _)) ...>))
25
+ PATTERN
26
+
27
+ def_node_matcher :factory_class_option_string?, <<~PATTERN
28
+ (send nil? :factory _ (hash <(pair (sym :class) $(str _)) ...>))
29
+ PATTERN
30
+
31
+ def on_send(node)
32
+ return unless inside_factory_bot_define?(node)
33
+
34
+ class_node = factory_class_option_symbol?(node) || factory_class_option_string?(node)
35
+
36
+ return unless class_node
37
+
38
+ class_name = class_node.value.to_s
39
+ puts class_name
40
+
41
+ return if class_exists?(class_name)
42
+
43
+ add_offense(class_node)
44
+ end
45
+
46
+ private def model_file_path(class_name)
47
+ "app/models/#{class_name.underscore}.rb"
48
+ end
49
+
50
+ private def class_exists?(class_name)
51
+ File.exist?(model_file_path(class_name))
52
+ end
53
+
54
+ private def inside_factory_bot_define?(node)
55
+ ancestors = node.each_ancestor(:block).to_a
56
+ ancestors.any? { |ancestor| ancestor.method_name == :define && ancestor.receiver&.const_name == 'FactoryBot' }
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -23,18 +23,20 @@ module RuboCop
23
23
  # association(:account)
24
24
  # end
25
25
  # end
26
- class FactoryBotRailsFactoryAssociationsCoverage < RuboCop::Cop::Base
26
+ class FactoryBotMissingAssociations < RuboCop::Cop::Base
27
27
  include RuboCop::Cop::ActiveRecordHelper
28
28
  extend AutoCorrector
29
29
 
30
30
  MSG = 'Ensure all associations of the model class are defined in the factory.'
31
31
 
32
+ RESTRICT_ON_SEND = %i[factory].freeze
33
+
32
34
  def_node_search :association_definitions, <<~PATTERN
33
35
  (send nil? :association ...)
34
36
  PATTERN
35
37
 
36
38
  def on_send(node)
37
- return unless factory_bot_define_block_with_class_option?(node)
39
+ return unless inside_factory_bot_define?(node)
38
40
 
39
41
  class_name = get_class_name(node)
40
42
  return unless class_name
@@ -104,14 +106,6 @@ module RuboCop
104
106
  "association(:#{property})"
105
107
  end
106
108
 
107
- private def factory_bot_define_block_with_class_option?(node)
108
- factory_method_call?(node) && inside_factory_bot_define?(node)
109
- end
110
-
111
- private def factory_method_call?(node)
112
- node.method_name == :factory
113
- end
114
-
115
109
  private def inside_factory_bot_define?(node)
116
110
  ancestors = node.each_ancestor(:block).to_a
117
111
  ancestors.any? { |ancestor| ancestor.method_name == :define && ancestor.receiver&.const_name == 'FactoryBot' }
@@ -13,22 +13,20 @@ module RuboCop
13
13
  # # good
14
14
  # factory :user, class: 'User' do
15
15
  # end
16
- class FactoryBotRailsClassOptionSpecified < RuboCop::Cop::Base
16
+ class FactoryBotMissingClassOption < RuboCop::Cop::Base
17
17
  extend AutoCorrector
18
18
 
19
19
  MSG = 'Specify a class option explicitly in FactoryBot factory.'
20
20
 
21
- def_node_matcher :factory_call?, <<~PATTERN
22
- (send nil? :factory ...)
23
- PATTERN
21
+ RESTRICT_ON_SEND = %i[factory].freeze
24
22
 
25
23
  def on_send(node)
26
- return unless factory_call?(node)
24
+ return unless inside_factory_bot_define?(node)
27
25
 
28
26
  # `factory`メソッドの呼び出しで、ハッシュ引数に`:class`キーが含まれているかを調べる
29
- class_option_specified = node.arguments.any? do |arg|
27
+ class_option_specified = node.arguments.any? { |arg|
30
28
  arg.hash_type? && arg.pairs.any? { |pair| pair.key.sym_type? && pair.key.children.first == :class }
31
- end
29
+ }
32
30
  return if class_option_specified
33
31
 
34
32
  add_offense(node.loc.selector) do |corrector|
@@ -39,6 +37,11 @@ module RuboCop
39
37
  corrector.insert_after(node.first_argument.loc.expression, ", class: '#{class_name}'")
40
38
  end
41
39
  end
40
+
41
+ private def inside_factory_bot_define?(node)
42
+ ancestors = node.each_ancestor(:block).to_a
43
+ ancestors.any? { |ancestor| ancestor.method_name == :define && ancestor.receiver&.const_name == 'FactoryBot' }
44
+ end
42
45
  end
43
46
  end
44
47
  end
@@ -26,12 +26,14 @@ module RuboCop
26
26
  # name { 'John Doe' }
27
27
  # end
28
28
  # end
29
- class FactoryBotRailsFactoryPropertiesCoverage < RuboCop::Cop::Base
29
+ class FactoryBotMissingProperties < RuboCop::Cop::Base
30
30
  include RuboCop::Cop::ActiveRecordHelper
31
31
  extend AutoCorrector
32
32
 
33
33
  MSG = 'Ensure all properties of the model class are defined in the factory.'
34
34
 
35
+ RESTRICT_ON_SEND = %i[factory].freeze
36
+
35
37
  def_node_search :sequence_definitions, <<~PATTERN
36
38
  (send nil? :sequence ...)
37
39
  PATTERN
@@ -41,7 +43,7 @@ module RuboCop
41
43
  PATTERN
42
44
 
43
45
  def on_send(node)
44
- return unless factory_bot_define_block_with_class_option?(node)
46
+ return unless inside_factory_bot_define?(node)
45
47
 
46
48
  class_name = get_class_name(node)
47
49
  return unless class_name
@@ -126,12 +128,15 @@ module RuboCop
126
128
  associations = extractor.extract(source)
127
129
  foreign_key_names =
128
130
  associations
131
+ .select { _1[:type] == :belongs_to }
129
132
  .map { |association|
130
133
  options = association[:options]
131
134
  if options[:foreign_key]
132
135
  options[:foreign_key]
133
136
  elsif options[:class_name]
134
137
  foreign_key_name(options[:class_name])
138
+ else
139
+ "#{association[:name]}_id"
135
140
  end
136
141
  }
137
142
  .compact
@@ -185,14 +190,6 @@ module RuboCop
185
190
  return property_names
186
191
  end
187
192
 
188
- private def factory_bot_define_block_with_class_option?(node)
189
- factory_method_call?(node) && inside_factory_bot_define?(node)
190
- end
191
-
192
- private def factory_method_call?(node)
193
- node.method_name == :factory
194
- end
195
-
196
193
  private def inside_factory_bot_define?(node)
197
194
  ancestors = node.each_ancestor(:block).to_a
198
195
  ancestors.any? { |ancestor| ancestor.method_name == :define && ancestor.receiver&.const_name == 'FactoryBot' }
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Momocop
6
+ # Ensures that FactoryBot factories has ordered property definitions.
7
+ # 1. Associations should be defined before other properties.
8
+ # 2. Associations and properties should be defined in alphabetical order.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # factory :user, class: 'User' do
13
+ # address { '123 Main St' }
14
+ # association :profile
15
+ # end
16
+ #
17
+ # # good
18
+ # factory :user, class: 'User' do
19
+ # association :profile
20
+ # address { '123 Main St' }
21
+ # end
22
+ #
23
+ # # bad
24
+ # factory :user, class: 'User' do
25
+ # association :profile
26
+ # association :account
27
+ # zipcode { '111-1111' }
28
+ # address { '123 Main St' }
29
+ # end
30
+ #
31
+ # # good
32
+ # factory :user, class: 'User' do
33
+ # association :account
34
+ # association :profile
35
+ # address { '123 Main St' }
36
+ # zipcode { '111-1111' }
37
+ # end
38
+ class FactoryBotPropertyOrder < RuboCop::Cop::Base
39
+ extend AutoCorrector
40
+ include RangeHelp
41
+
42
+ include ::Sevencop::CopConcerns::Ordered
43
+
44
+ MSG = 'Sort properties and associations alphabetically.'
45
+
46
+ RESTRICT_ON_SEND = %i[factory].freeze
47
+
48
+ def on_send(node)
49
+ return unless inside_factory_bot_define?(node)
50
+
51
+ block_node = node.block_node
52
+ entire_definitions = defined_properties(block_node)
53
+
54
+ sections =
55
+ entire_definitions
56
+ .slice_when { |a, b| b.loc.last_line - a.loc.last_line > 1 }
57
+ .select { |definitions| definitions.size >= 2 }
58
+
59
+ sections.each do |definitions|
60
+ add_offense(definitions.last) do |corrector|
61
+ (a, b) =
62
+ definitions
63
+ .lazy
64
+ .each_cons(2)
65
+ .find { |a, b| (order(a) <=> order(b)) == 1 }
66
+
67
+ break unless a && b
68
+
69
+ swap(
70
+ range_with_comments_and_lines(a),
71
+ range_with_comments_and_lines(b),
72
+ corrector:
73
+ )
74
+ end
75
+ end
76
+ end
77
+
78
+ private def order(node)
79
+ group = definition_type(node) == :association ? 0 : 1
80
+ index = definition_name(node)
81
+ [group, index]
82
+ end
83
+
84
+ private def defined_properties(block_node)
85
+ body_node = block_node&.children&.last
86
+ body_node&.children&.select { |node| definition_node?(node) }
87
+ end
88
+
89
+ private def definition_node?(node)
90
+ node.send_type? || (node.block_type? && node.children.first.send_type?)
91
+ end
92
+
93
+ private def definition_type(node)
94
+ send_node = node.send_type? ? node : node.children.first
95
+ if %i[association sequence].include? send_node.method_name
96
+ send_node.method_name
97
+ else
98
+ :property
99
+ end
100
+ end
101
+
102
+ private def definition_name(node)
103
+ send_node = node.send_type? ? node : node.children.first
104
+ if %i[association sequence].include? send_node.method_name
105
+ send_node.arguments.first.value
106
+ else
107
+ send_node.method_name.to_sym
108
+ end
109
+ end
110
+
111
+ private def inside_factory_bot_define?(node)
112
+ ancestors = node.each_ancestor(:block).to_a
113
+ ancestors.any? { |ancestor| ancestor.method_name == :define && ancestor.receiver&.const_name == 'FactoryBot' }
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: momocop
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - supermomonga
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-02-18 00:00:00.000000000 Z
11
+ date: 2024-02-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sevencop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  description: Convention focused opinionated custom cops for RuboCop.
56
70
  email:
57
71
  - hi@supermomonga.com
@@ -66,7 +80,10 @@ files:
66
80
  - CODEOWNERS
67
81
  - CODE_OF_CONDUCT.md
68
82
  - LICENSE.txt
83
+ - README.ja.md
84
+ - README.ja.md.erb
69
85
  - README.md
86
+ - README.md.erb
70
87
  - Rakefile
71
88
  - config/default.yml
72
89
  - lib/momocop.rb
@@ -74,10 +91,11 @@ files:
74
91
  - lib/momocop/config_injector.rb
75
92
  - lib/momocop/enum_extractor.rb
76
93
  - lib/momocop/version.rb
77
- - lib/rubocop/cop/momocop/factory_bot_rails_class_name.rb
78
- - lib/rubocop/cop/momocop/factory_bot_rails_class_option_specified.rb
79
- - lib/rubocop/cop/momocop/factory_bot_rails_factory_associations_coverage.rb
80
- - lib/rubocop/cop/momocop/factory_bot_rails_factory_properties_coverage.rb
94
+ - lib/rubocop/cop/momocop/factory_bot_class_existence.rb
95
+ - lib/rubocop/cop/momocop/factory_bot_missing_associations.rb
96
+ - lib/rubocop/cop/momocop/factory_bot_missing_class_option.rb
97
+ - lib/rubocop/cop/momocop/factory_bot_missing_properties.rb
98
+ - lib/rubocop/cop/momocop/factory_bot_property_order.rb
81
99
  - sig/momocop.rbs
82
100
  homepage: https://github.com/supermomonga/momocop
83
101
  licenses:
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module RuboCop
4
- module Cop
5
- module Momocop
6
- # Ensures that FactoryBot factories use `ClassName.name` for the class option instead of symbol or string literals.
7
- #
8
- # @example
9
- # # bad
10
- # factory :user, class: :User do
11
- # end
12
- #
13
- # factory :admin, class: 'Admin' do
14
- # end
15
- #
16
- # factory :moderator, class: Moderator.to_s do
17
- # end
18
- #
19
- # # good
20
- # factory :user, class: User.name do
21
- # end
22
- #
23
- # factory :admin, class: Admin.name do
24
- # end
25
- #
26
- # factory :moderator, class: Moderator.name do
27
- # end
28
- class FactoryBotRailsClassName < RuboCop::Cop::Base
29
- extend AutoCorrector
30
-
31
- MSG = 'Use `ClassName.name` for the class option instead of a symbol, string literal, or `ClassName.to_s`.'
32
-
33
- def_node_matcher :factory_class_option_symbol?, <<~PATTERN
34
- (send nil? :factory _ (hash <(pair (sym :class) $(sym _)) ...>))
35
- PATTERN
36
-
37
- def_node_matcher :factory_class_option_string?, <<~PATTERN
38
- (send nil? :factory _ (hash <(pair (sym :class) $(str _)) ...>))
39
- PATTERN
40
-
41
- def_node_matcher :factory_class_option_to_s?, <<~PATTERN
42
- (send nil? :factory _ (hash <(pair (sym :class) $(send _ :to_s)) ...>))
43
- PATTERN
44
-
45
- def on_send(node)
46
- factory_class_option_symbol?(node) do |class_option|
47
- add_offense(class_option) do |corrector|
48
- corrector.replace(class_option, "#{class_option.value.capitalize}.name")
49
- end
50
- end
51
-
52
- factory_class_option_string?(node) do |class_option|
53
- class_name = class_option.value.delete_prefix("'").delete_suffix("'").capitalize
54
- add_offense(class_option) do |corrector|
55
- corrector.replace(class_option, "#{class_name}.name")
56
- end
57
- end
58
-
59
- factory_class_option_to_s?(node) do |class_option|
60
- class_name = class_option.receiver.const_name
61
- add_offense(class_option) do |corrector|
62
- corrector.replace(class_option, "#{class_name}.name")
63
- end
64
- end
65
- end
66
- end
67
- end
68
- end
69
- end