rubocop-isucon 0.1.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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/gh-pages.yml +44 -0
  3. data/.github/workflows/test.yml +91 -0
  4. data/.gitignore +13 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +43 -0
  7. data/.yardopts +7 -0
  8. data/CHANGELOG.md +6 -0
  9. data/Gemfile +8 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +108 -0
  12. data/Rakefile +35 -0
  13. data/benchmark/README.md +69 -0
  14. data/benchmark/memorize.rb +86 -0
  15. data/benchmark/parse_table.rb +103 -0
  16. data/benchmark/shell.rb +26 -0
  17. data/bin/console +15 -0
  18. data/bin/setup +8 -0
  19. data/config/default.yml +83 -0
  20. data/config/enable-only-performance.yml +30 -0
  21. data/gemfiles/activerecord_6_1.gemfile +14 -0
  22. data/gemfiles/activerecord_7_0.gemfile +14 -0
  23. data/lib/rubocop/cop/isucon/correctors/mysql2_n_plus_one_query_corrector/correctable_methods.rb +66 -0
  24. data/lib/rubocop/cop/isucon/correctors/mysql2_n_plus_one_query_corrector/replace_methods.rb +127 -0
  25. data/lib/rubocop/cop/isucon/correctors/mysql2_n_plus_one_query_corrector.rb +112 -0
  26. data/lib/rubocop/cop/isucon/mixin/database_methods.rb +59 -0
  27. data/lib/rubocop/cop/isucon/mixin/mysql2_xquery_methods.rb +176 -0
  28. data/lib/rubocop/cop/isucon/mixin/sinatra_methods.rb +37 -0
  29. data/lib/rubocop/cop/isucon/mysql2/join_without_index.rb +100 -0
  30. data/lib/rubocop/cop/isucon/mysql2/many_join_table.rb +86 -0
  31. data/lib/rubocop/cop/isucon/mysql2/n_plus_one_query.rb +179 -0
  32. data/lib/rubocop/cop/isucon/mysql2/prepare_execute.rb +136 -0
  33. data/lib/rubocop/cop/isucon/mysql2/select_asterisk.rb +171 -0
  34. data/lib/rubocop/cop/isucon/mysql2/where_without_index.rb +105 -0
  35. data/lib/rubocop/cop/isucon/shell/backtick.rb +36 -0
  36. data/lib/rubocop/cop/isucon/shell/system.rb +36 -0
  37. data/lib/rubocop/cop/isucon/sinatra/disable_logging.rb +83 -0
  38. data/lib/rubocop/cop/isucon/sinatra/logger.rb +52 -0
  39. data/lib/rubocop/cop/isucon/sinatra/rack_logger.rb +58 -0
  40. data/lib/rubocop/cop/isucon/sinatra/serve_static_file.rb +73 -0
  41. data/lib/rubocop/cop/isucon_cops.rb +20 -0
  42. data/lib/rubocop/isucon/database_connection.rb +42 -0
  43. data/lib/rubocop/isucon/gda/client.rb +184 -0
  44. data/lib/rubocop/isucon/gda/gda_ext.rb +119 -0
  45. data/lib/rubocop/isucon/gda/join_condition.rb +25 -0
  46. data/lib/rubocop/isucon/gda/join_operand.rb +46 -0
  47. data/lib/rubocop/isucon/gda/node_location.rb +42 -0
  48. data/lib/rubocop/isucon/gda/node_patcher.rb +101 -0
  49. data/lib/rubocop/isucon/gda/where_condition.rb +73 -0
  50. data/lib/rubocop/isucon/gda/where_operand.rb +32 -0
  51. data/lib/rubocop/isucon/gda.rb +28 -0
  52. data/lib/rubocop/isucon/inject.rb +20 -0
  53. data/lib/rubocop/isucon/memorize_methods.rb +38 -0
  54. data/lib/rubocop/isucon/version.rb +7 -0
  55. data/lib/rubocop/isucon.rb +20 -0
  56. data/lib/rubocop-isucon.rb +16 -0
  57. data/rubocop-isucon.gemspec +52 -0
  58. metadata +286 -0
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Isucon
6
+ module Mixin
7
+ # Helper methods for `db.xquery` or `db.query` in AST
8
+ module Mysql2XqueryMethods
9
+ extend NodePattern::Macros
10
+
11
+ # @!method find_xquery(node)
12
+ # @param node [RuboCop::AST::Node]
13
+ def_node_search :find_xquery, <<~PATTERN
14
+ (send (send nil? _) {:xquery | :query} (${str dstr lvar ivar cvar} $...) ...)
15
+ PATTERN
16
+
17
+ NON_STRING_WARNING_MSG = "Warning: non-string was passed to `query` or `xquery` 1st argument. " \
18
+ "So argument doesn't parsed as SQL (%<file_path>s:%<line_num>d)"
19
+
20
+ # @param node [RuboCop::AST::Node]
21
+ # @yieldparam type [Symbol] Node type. one of `:str`, `:dstr`
22
+ # @yieldparam root_gda [RuboCop::Isucon::GDA::Client,nil]
23
+ #
24
+ # @note If arguments of `db.xquery` isn't string, `root_gda` is `nil`
25
+ def with_xquery(node)
26
+ find_xquery(node) do |type, params|
27
+ sql = xquery_param(type: type, params: params)
28
+
29
+ unless sql
30
+ warn format(NON_STRING_WARNING_MSG, file_path: processed_source.file_path, line_num: node.loc.expression.line)
31
+ end
32
+
33
+ root_gda = sql ? RuboCop::Isucon::GDA::Client.new(sql) : nil
34
+
35
+ yield type, root_gda
36
+ end
37
+ end
38
+
39
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
40
+ # @param node [RuboCop::AST::Node]
41
+ # @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
42
+ # @return [Parser::Source::Range,nil]
43
+ def offense_location(type:, node:, gda_location:)
44
+ return nil unless gda_location
45
+
46
+ begin_pos = begin_position_from_gda_location(type: type, node: node, gda_location: gda_location)
47
+ return nil unless begin_pos
48
+
49
+ end_pos = begin_pos + gda_location.length
50
+ Parser::Source::Range.new(node.loc.expression.source_buffer, begin_pos, end_pos)
51
+ end
52
+
53
+ private
54
+
55
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
56
+ # @param params [Array<RuboCop::AST::Node>]
57
+ # @return [String,nil]
58
+ def xquery_param(type:, params:)
59
+ case type
60
+ when :str
61
+ return params[0]
62
+ when :dstr
63
+ if params.all? { |param| param.respond_to?(:value) }
64
+ # heredoc
65
+ return params.map(&:value).join
66
+ end
67
+ end
68
+ nil
69
+ end
70
+
71
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
72
+ # @param node [RuboCop::AST::Node]
73
+ # @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
74
+ # @return [Integer,nil]
75
+ def begin_position_from_gda_location(type:, node:, gda_location:)
76
+ case type
77
+ when :str
78
+ return begin_position_from_gda_location_for_str(node: node, gda_location: gda_location)
79
+ when :dstr
80
+ return begin_position_from_gda_location_for_dstr(node: node, gda_location: gda_location)
81
+ end
82
+
83
+ nil
84
+ end
85
+
86
+ # @param node [RuboCop::AST::Node]
87
+ # @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
88
+ # @return [Integer,nil]
89
+ def begin_position_from_gda_location_for_str(node:, gda_location:)
90
+ str_node = node.child_nodes[1]
91
+ return nil unless str_node&.str_type?
92
+
93
+ str_node.loc.begin.end_pos + gda_location.begin_pos
94
+ end
95
+
96
+ # @param node [RuboCop::AST::Node]
97
+ # @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
98
+ # @return [Integer,nil]
99
+ def begin_position_from_gda_location_for_dstr(node:, gda_location:)
100
+ dstr_node = node.child_nodes[1]
101
+ return nil unless dstr_node&.dstr_type?
102
+
103
+ str_node = find_str_node_from_gda_location(dstr_node: dstr_node, gda_location: gda_location)
104
+ index = str_node.value.index(gda_location.body)
105
+ return nil unless index
106
+
107
+ str_node_begin_pos(str_node) + index + heredoc_indent_level(node)
108
+ end
109
+
110
+ # @param str_node [RuboCop::AST::StrNode]
111
+ # @return [Integer]
112
+ def str_node_begin_pos(str_node)
113
+ begin_pos = str_node.loc.expression.begin_pos
114
+
115
+ # e.g.
116
+ # db.xquery(
117
+ # "SELECT * " \
118
+ # "FROM users " \
119
+ # "LIMIT 10"
120
+ # )
121
+ return begin_pos + 1 if str_node.loc.expression.source_buffer.source[begin_pos] == '"'
122
+
123
+ begin_pos
124
+ end
125
+
126
+ # @param dstr_node [RuboCop::AST::DstrNode]
127
+ # @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
128
+ # @return [RuboCop::AST::StrNode,nil]
129
+ def find_str_node_from_gda_location(dstr_node:, gda_location:)
130
+ return nil unless dstr_node
131
+
132
+ begin_pos = 0
133
+ dstr_node.child_nodes.each do |str_node|
134
+ return str_node if begin_pos <= gda_location.begin_pos && gda_location.begin_pos < begin_pos + str_node.value.length
135
+
136
+ begin_pos += str_node.value.length
137
+ end
138
+ nil
139
+ end
140
+
141
+ # @param node [RuboCop::AST::Node]
142
+ # @return [Integer]
143
+ def heredoc_indent_level(node)
144
+ dstr_node = node.child_nodes[1]
145
+ return 0 unless dstr_node&.dstr_type?
146
+
147
+ heredoc_indent_type = heredoc_indent_type(node)
148
+ return 0 unless heredoc_indent_type == "~"
149
+
150
+ heredoc_body = dstr_node.loc.heredoc_body.source
151
+ indent_level(heredoc_body)
152
+ end
153
+
154
+ # @param str [String]
155
+ # @return [Integer]
156
+ # @see https://github.com/rubocop/rubocop/blob/v1.21.0/lib/rubocop/cop/mixin/heredoc.rb#L23-L28
157
+ def indent_level(str)
158
+ indentations = str.lines.
159
+ map { |line| line[/^\s*/] }.
160
+ reject { |line| line.end_with?("\n") }
161
+ indentations.empty? ? 0 : indentations.min_by(&:size).size
162
+ end
163
+
164
+ # Returns '~', '-' or nil
165
+ #
166
+ # @param node [RuboCop::AST::Node]
167
+ # @return [String,nil] '~', '-' or `nil`
168
+ # @see https://github.com/rubocop/rubocop/blob/v1.21.0/lib/rubocop/cop/layout/heredoc_indentation.rb#L146-L149
169
+ def heredoc_indent_type(node)
170
+ node.source[/<<([~-])/, 1]
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Isucon
6
+ module Mixin
7
+ # Helper methods for {RuboCop::Cop::Isucon::Sinatra}
8
+ module SinatraMethods
9
+ extend NodePattern::Macros
10
+
11
+ # @!method subclass_of_sinatra_base?(node)
12
+ # Whether match to `class AnyClass < Sinatra::Base` node
13
+ # @param node [RuboCop::AST::Node]
14
+ # @return [Boolean]
15
+ def_node_matcher :subclass_of_sinatra_base?, <<~PATTERN
16
+ (class (const nil? _) (const (const nil? :Sinatra) :Base) ...)
17
+ PATTERN
18
+
19
+ # @!method subclass_of_sinatra_base_contains_logging?(node)
20
+ # Whether match to `class AnyClass < Sinatra::Base` node and contains :logging configuration
21
+ # @param node [RuboCop::AST::Node]
22
+ # @return [Boolean]
23
+ def_node_matcher :subclass_of_sinatra_base_contains_logging?, <<~PATTERN
24
+ (class (const nil? _) (const (const nil? :Sinatra) :Base) ... `(send nil? _ (sym :logging)))
25
+ PATTERN
26
+
27
+ # Whether parent node match to `class AnyClass < Sinatra::Base` node
28
+ # @param node [RuboCop::AST::Node]
29
+ # @return [Boolean]
30
+ def parent_is_sinatra_app?(node)
31
+ node.each_ancestor.any? { |ancestor| subclass_of_sinatra_base?(ancestor) }
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Isucon
6
+ module Mysql2
7
+ # Check for `JOIN` without index
8
+ #
9
+ # @note If `Database` isn't configured, this cop's feature (offense detection and auto-correct) will not be available.
10
+ #
11
+ # @example
12
+ # # bad (user_id is not indexed)
13
+ # db.xquery('SELECT id, title FROM articles JOIN users ON users.id = articles.user_id')
14
+ #
15
+ # # good (user_id is indexed)
16
+ # db.xquery('SELECT id, title FROM articles JOIN users ON users.id = articles.user_id')
17
+ #
18
+ class JoinWithoutIndex < Base
19
+ include Mixin::DatabaseMethods
20
+ include Mixin::Mysql2XqueryMethods
21
+
22
+ MSG = "This join clause doesn't seem to have an index. " \
23
+ "(e.g. `ALTER TABLE %<table_name>s ADD INDEX index_%<column_name>s (%<column_name>s)`)"
24
+
25
+ # @param node [RuboCop::AST::Node]
26
+ def on_send(node)
27
+ with_error_handling(node) do
28
+ return unless enabled_database?
29
+
30
+ with_xquery(node) do |type, root_gda|
31
+ check_and_register_offence(type: type, root_gda: root_gda, node: node)
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
39
+ # @param root_gda [RuboCop::Isucon::GDA::Client]
40
+ # @param node [RuboCop::AST::Node]
41
+ def check_and_register_offence(type:, root_gda:, node:)
42
+ return unless root_gda
43
+
44
+ root_gda.visit_all do |gda|
45
+ gda.join_conditions.each do |join_condition|
46
+ join_operand = join_operand_without_index(join_condition)
47
+ next unless join_operand
48
+
49
+ register_offense(type: type, node: node, join_operand: join_operand)
50
+ end
51
+ end
52
+ end
53
+
54
+ # @param join_condition [RuboCop::Isucon::GDA::JoinCondition]
55
+ # @return [RuboCop::Isucon::GDA::JoinOperand,nil]
56
+ def join_operand_without_index(join_condition)
57
+ join_condition.operands.each do |join_operand|
58
+ next unless join_operand.table_name
59
+
60
+ unless indexed_column?(table_name: join_operand.table_name, column_name: join_operand.column_name)
61
+ return join_operand
62
+ end
63
+ end
64
+
65
+ nil
66
+ end
67
+
68
+ # @param table_name [String]
69
+ # @param column_name [String]
70
+ # @return [Boolean]
71
+ def indexed_column?(table_name:, column_name:)
72
+ primary_keys = connection.primary_keys(table_name)
73
+
74
+ return true if primary_keys&.first == column_name
75
+
76
+ indexes = connection.indexes(table_name)
77
+ index_first_columns = indexes.map { |index| index.columns[0] }
78
+ index_first_columns.include?(column_name)
79
+ end
80
+
81
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
82
+ # @param node [RuboCop::AST::Node]
83
+ # @param join_operand [RuboCop::Isucon::GDA::JoinOperand]
84
+ def register_offense(type:, node:, join_operand:)
85
+ loc = offense_location(type: type, node: node, gda_location: join_operand.node.location)
86
+ return unless loc
87
+
88
+ message = offense_message(join_operand)
89
+ add_offense(loc, message: message)
90
+ end
91
+
92
+ # @param join_operand [RuboCop::Isucon::GDA::JoinOperand]
93
+ def offense_message(join_operand)
94
+ format(MSG, table_name: join_operand.table_name, column_name: join_operand.column_name)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Isucon
6
+ module Mysql2
7
+ # Check if SQL contains many JOINs
8
+ #
9
+ # @example CountTables: 3 (default)
10
+ # # bad
11
+ # totals = db.xquery(
12
+ # "SELECT IFNULL(SUM(`submissions`.`score`), 0) AS `total_score`" \
13
+ # " FROM `users`" \
14
+ # " JOIN `registrations` ON `users`.`id` = `registrations`.`user_id`" \
15
+ # " JOIN `courses` ON `registrations`.`course_id` = `courses`.`id`" \
16
+ # " LEFT JOIN `classes` ON `courses`.`id` = `classes`.`course_id`" \
17
+ # " LEFT JOIN `submissions` ON `users`.`id` = `submissions`.`user_id` AND `submissions`.`class_id` = `classes`.`id`" \
18
+ # " WHERE `courses`.`id` = ?" \
19
+ # " GROUP BY `users`.`id`",
20
+ # course[:id]
21
+ # ).map { |_| _[:total_score] }
22
+ #
23
+ # # good
24
+ # registration_users_count =
25
+ # db.xquery("SELECT COUNT(`user_id`) AS cnt FROM `registrations` WHERE `course_id` = ?", course[:id]).first[:cnt]
26
+ #
27
+ # totals = db.xquery(<<~SQL, course[:id]).map { |_| _[:total_score] }
28
+ # SELECT IFNULL(SUM(`submissions`.`score`), 0) AS `total_score`
29
+ # FROM `submissions`
30
+ # JOIN `classes` ON `classes`.`id` = `submissions`.`class_id`
31
+ # WHERE `classes`.`course_id` = ?
32
+ # GROUP BY `submissions`.`user_id`
33
+ # SQL
34
+ #
35
+ # if totals.count < registration_users_count
36
+ # no_submissions_count = registration_users_count - totals.count
37
+ # totals += [0] * no_submissions_count
38
+ # end
39
+ #
40
+ # @example CountTables: 5
41
+ # # good
42
+ # totals = db.xquery(
43
+ # "SELECT IFNULL(SUM(`submissions`.`score`), 0) AS `total_score`" \
44
+ # " FROM `users`" \
45
+ # " JOIN `registrations` ON `users`.`id` = `registrations`.`user_id`" \
46
+ # " JOIN `courses` ON `registrations`.`course_id` = `courses`.`id`" \
47
+ # " LEFT JOIN `classes` ON `courses`.`id` = `classes`.`course_id`" \
48
+ # " LEFT JOIN `submissions` ON `users`.`id` = `submissions`.`user_id` AND `submissions`.`class_id` = `classes`.`id`" \
49
+ # " WHERE `courses`.`id` = ?" \
50
+ # " GROUP BY `users`.`id`",
51
+ # course[:id]
52
+ # ).map { |_| _[:total_score] }
53
+ #
54
+ class ManyJoinTable < Base
55
+ include Mixin::Mysql2XqueryMethods
56
+
57
+ MSG = "Avoid SQL with lots of JOINs"
58
+
59
+ # @param node [RuboCop::AST::Node]
60
+ def on_send(node)
61
+ with_xquery(node) do |_, root_gda|
62
+ check_and_register_offence(root_gda: root_gda, node: node)
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ # @param root_gda [RuboCop::Isucon::GDA::Client]
69
+ # @param node [RuboCop::AST::Node]
70
+ def check_and_register_offence(root_gda:, node:)
71
+ return unless root_gda
72
+
73
+ root_gda.visit_all do |gda|
74
+ add_offense(node) if gda.table_names.count > count_tables
75
+ end
76
+ end
77
+
78
+ # @return [Integer]
79
+ def count_tables
80
+ cop_config["CountTables"]
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Isucon
6
+ module Mysql2
7
+ # rubocop:disable Layout/LineLength
8
+
9
+ # Checks that N+1 query is not used
10
+ #
11
+ # @note If `Database` isn't configured, auto-correct will not be available. (Only offense detection can be used)
12
+ #
13
+ # @note For the number of N+1 queries that can be detected by this cop, there are too few that can be corrected automatically
14
+ #
15
+ # @example
16
+ # # bad
17
+ # reservations = db.xquery('SELECT * FROM `reservations` WHERE `schedule_id` = ?', schedule_id).map do |reservation|
18
+ # reservation[:user] = db.xquery('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', id).first
19
+ # reservation
20
+ # end
21
+ #
22
+ # # good
23
+ # rows = db.xquery(<<~SQL, schedule_id)
24
+ # SELECT
25
+ # r.id AS reservation_id,
26
+ # r.schedule_id AS reservation_schedule_id,
27
+ # r.user_id AS reservation_user_id,
28
+ # r.created_at AS reservation_created_at,
29
+ # u.id AS user_id,
30
+ # u.email AS user_email,
31
+ # u.nickname AS user_nickname,
32
+ # u.staff AS user_staff,
33
+ # u.created_at AS user_created_at
34
+ # FROM `reservations` AS r
35
+ # INNER JOIN users u ON u.id = r.user_id
36
+ # WHERE r.schedule_id = ?
37
+ # SQL
38
+ #
39
+ # # bad
40
+ # courses.map do |course|
41
+ # teacher = db.xquery('SELECT * FROM `users` WHERE `id` = ?', course[:teacher_id]).first
42
+ # end
43
+ #
44
+ # # good
45
+ # # This is similar to ActiveRecord's preload
46
+ # # c.f. https://guides.rubyonrails.org/active_record_querying.html#preload
47
+ # courses.map do |course|
48
+ # @users_by_id ||= db.xquery('SELECT * FROM `users` WHERE `id` IN (?)', courses.map { |course| course[:teacher_id] }).each_with_object({}) { |v, hash| hash[v[:id]] = v }
49
+ # teacher = @users_by_id[course[:teacher_id]]
50
+ # end
51
+ class NPlusOneQuery < Base
52
+ # rubocop:enable Layout/LineLength
53
+
54
+ include Mixin::DatabaseMethods
55
+ include Mixin::Mysql2XqueryMethods
56
+
57
+ extend AutoCorrector
58
+
59
+ MSG = "This looks like N+1 query."
60
+
61
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L38
62
+ POST_CONDITION_LOOP_TYPES = %i[while_post until_post].freeze
63
+
64
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L39
65
+ LOOP_TYPES = (POST_CONDITION_LOOP_TYPES + %i[while until for]).freeze
66
+
67
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L41
68
+ ENUMERABLE_METHOD_NAMES = (Enumerable.instance_methods + [:each]).to_set.freeze
69
+
70
+ def_node_matcher :csv_loop?, <<~PATTERN
71
+ (block
72
+ (send (const nil? :CSV) :parse ...)
73
+ ...)
74
+ PATTERN
75
+
76
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L68
77
+ def_node_matcher :kernel_loop?, <<~PATTERN
78
+ (block
79
+ (send {nil? (const nil? :Kernel)} :loop)
80
+ ...)
81
+ PATTERN
82
+
83
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L74
84
+ def_node_matcher :enumerable_loop?, <<~PATTERN
85
+ (block
86
+ (send $_ #enumerable_method? ...)
87
+ ...)
88
+ PATTERN
89
+
90
+ # @param node [RuboCop::AST::Node]
91
+ def on_send(node) # rubocop:disable Metrics/MethodLength
92
+ with_error_handling(node) do
93
+ with_xquery(node) do |type, root_gda|
94
+ receiver, = *node.children
95
+
96
+ next unless receiver.send_type?
97
+
98
+ parent = parent_loop_node(receiver)
99
+ next unless parent
100
+
101
+ next if or_assignment_to_instance_variable?(node)
102
+
103
+ add_offense(receiver) do |corrector|
104
+ perform_autocorrect(
105
+ corrector: corrector, current_node: receiver,
106
+ parent_node: parent, type: type, gda: root_gda
107
+ )
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ # Whether match to `@instance_var ||=`
116
+ # @param node [RuboCop::AST::Node]
117
+ # @return [Boolean]
118
+ def or_assignment_to_instance_variable?(node)
119
+ _or_assignment_to_instance_variable?(node.parent&.parent) ||
120
+ _or_assignment_to_instance_variable?(node.parent&.parent&.parent)
121
+ end
122
+
123
+ # Whether match to `@instance_var ||=`
124
+ # @param node [RuboCop::AST::Node]
125
+ # @return [Boolean]
126
+ def _or_assignment_to_instance_variable?(node)
127
+ node&.or_asgn_type? && node.child_nodes&.first&.ivasgn_type?
128
+ end
129
+
130
+ # @param node [RuboCop::AST::Node]
131
+ # @return [RuboCop::AST::Node]
132
+ def parent_loop_node(node)
133
+ node.each_ancestor.find { |ancestor| loop?(ancestor, node) }
134
+ end
135
+
136
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L106
137
+ def loop?(ancestor, node)
138
+ keyword_loop?(ancestor.type) ||
139
+ kernel_loop?(ancestor) ||
140
+ node_within_enumerable_loop?(node, ancestor) ||
141
+ csv_loop?(ancestor)
142
+ end
143
+
144
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L112
145
+ def keyword_loop?(type)
146
+ LOOP_TYPES.include?(type)
147
+ end
148
+
149
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L116
150
+ def node_within_enumerable_loop?(node, ancestor)
151
+ enumerable_loop?(ancestor) do |receiver|
152
+ receiver != node && !receiver&.descendants&.include?(node)
153
+ end
154
+ end
155
+
156
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L130
157
+ def enumerable_method?(method_name)
158
+ ENUMERABLE_METHOD_NAMES.include?(method_name)
159
+ end
160
+
161
+ # @param corrector [RuboCop::Cop::Corrector]
162
+ # @param current_node [RuboCop::AST::Node]
163
+ # @param parent_node [RuboCop::AST::Node]
164
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
165
+ # @param gda [RuboCop::Isucon::GDA::Client]
166
+ def perform_autocorrect(corrector:, current_node:, parent_node:, type:, gda:)
167
+ return unless enabled_database?
168
+
169
+ corrector = Correctors::Mysql2NPlusOneQueryCorrector.new(
170
+ corrector: corrector, current_node: current_node,
171
+ parent_node: parent_node, type: type, gda: gda, connection: connection
172
+ )
173
+ corrector.correct
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end