rubocop-isucon 0.1.0 → 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.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -1
  3. data/README.md +17 -6
  4. data/config/default.yml +36 -0
  5. data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector → n_plus_one_query_corrector}/correctable_methods.rb +1 -1
  6. data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector → n_plus_one_query_corrector}/replace_methods.rb +1 -1
  7. data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector.rb → n_plus_one_query_corrector.rb} +15 -4
  8. data/lib/rubocop/cop/isucon/mixin/join_without_index_methods.rb +87 -0
  9. data/lib/rubocop/cop/isucon/mixin/many_join_table_methods.rb +39 -0
  10. data/lib/rubocop/cop/isucon/mixin/mysql2_xquery_methods.rb +7 -116
  11. data/lib/rubocop/cop/isucon/mixin/n_plus_one_query_methods.rb +153 -0
  12. data/lib/rubocop/cop/isucon/mixin/offense_location_methods.rb +130 -0
  13. data/lib/rubocop/cop/isucon/mixin/select_asterisk_methods.rb +148 -0
  14. data/lib/rubocop/cop/isucon/mixin/sqlite3_execute_methods.rb +67 -0
  15. data/lib/rubocop/cop/isucon/mixin/where_without_index_methods.rb +96 -0
  16. data/lib/rubocop/cop/isucon/mysql2/join_without_index.rb +4 -67
  17. data/lib/rubocop/cop/isucon/mysql2/many_join_table.rb +1 -26
  18. data/lib/rubocop/cop/isucon/mysql2/n_plus_one_query.rb +4 -114
  19. data/lib/rubocop/cop/isucon/mysql2/select_asterisk.rb +1 -135
  20. data/lib/rubocop/cop/isucon/mysql2/where_without_index.rb +5 -70
  21. data/lib/rubocop/cop/isucon/sqlite3/join_without_index.rb +37 -0
  22. data/lib/rubocop/cop/isucon/sqlite3/many_join_table.rb +61 -0
  23. data/lib/rubocop/cop/isucon/sqlite3/n_plus_one_query.rb +70 -0
  24. data/lib/rubocop/cop/isucon/sqlite3/select_asterisk.rb +37 -0
  25. data/lib/rubocop/cop/isucon/sqlite3/where_without_index.rb +40 -0
  26. data/lib/rubocop/cop/isucon_cops.rb +13 -1
  27. data/lib/rubocop/isucon/version.rb +1 -1
  28. metadata +17 -5
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Isucon
6
+ module Mixin
7
+ # Calculate offense location from Ruby and SQL ASTs
8
+ module OffenceLocationMethods
9
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
10
+ # @param node [RuboCop::AST::Node]
11
+ # @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
12
+ # @return [Parser::Source::Range,nil]
13
+ def offense_location(type:, node:, gda_location:)
14
+ return nil unless gda_location
15
+
16
+ begin_pos = begin_position_from_gda_location(type: type, node: node, gda_location: gda_location)
17
+ return nil unless begin_pos
18
+
19
+ end_pos = begin_pos + gda_location.length
20
+ Parser::Source::Range.new(node.loc.expression.source_buffer, begin_pos, end_pos)
21
+ end
22
+
23
+ private
24
+
25
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
26
+ # @param node [RuboCop::AST::Node]
27
+ # @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
28
+ # @return [Integer,nil]
29
+ def begin_position_from_gda_location(type:, node:, gda_location:)
30
+ case type
31
+ when :str
32
+ return begin_position_from_gda_location_for_str(node: node, gda_location: gda_location)
33
+ when :dstr
34
+ return begin_position_from_gda_location_for_dstr(node: node, gda_location: gda_location)
35
+ end
36
+
37
+ nil
38
+ end
39
+
40
+ # @param node [RuboCop::AST::Node]
41
+ # @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
42
+ # @return [Integer,nil]
43
+ def begin_position_from_gda_location_for_str(node:, gda_location:)
44
+ str_node = node.child_nodes[1]
45
+ return nil unless str_node&.str_type?
46
+
47
+ str_node.loc.begin.end_pos + gda_location.begin_pos
48
+ end
49
+
50
+ # @param node [RuboCop::AST::Node]
51
+ # @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
52
+ # @return [Integer,nil]
53
+ def begin_position_from_gda_location_for_dstr(node:, gda_location:)
54
+ dstr_node = node.child_nodes[1]
55
+ return nil unless dstr_node&.dstr_type?
56
+
57
+ str_node = find_str_node_from_gda_location(dstr_node: dstr_node, gda_location: gda_location)
58
+ index = str_node.value.index(gda_location.body)
59
+ return nil unless index
60
+
61
+ str_node_begin_pos(str_node) + index + heredoc_indent_level(node)
62
+ end
63
+
64
+ # @param str_node [RuboCop::AST::StrNode]
65
+ # @return [Integer]
66
+ def str_node_begin_pos(str_node)
67
+ begin_pos = str_node.loc.expression.begin_pos
68
+
69
+ # e.g.
70
+ # db.xquery(
71
+ # "SELECT * " \
72
+ # "FROM users " \
73
+ # "LIMIT 10"
74
+ # )
75
+ return begin_pos + 1 if str_node.loc.expression.source_buffer.source[begin_pos] == '"'
76
+
77
+ begin_pos
78
+ end
79
+
80
+ # @param dstr_node [RuboCop::AST::DstrNode]
81
+ # @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
82
+ # @return [RuboCop::AST::StrNode,nil]
83
+ def find_str_node_from_gda_location(dstr_node:, gda_location:)
84
+ return nil unless dstr_node
85
+
86
+ begin_pos = 0
87
+ dstr_node.child_nodes.each do |str_node|
88
+ return str_node if begin_pos <= gda_location.begin_pos && gda_location.begin_pos < begin_pos + str_node.value.length
89
+
90
+ begin_pos += str_node.value.length
91
+ end
92
+ nil
93
+ end
94
+
95
+ # @param node [RuboCop::AST::Node]
96
+ # @return [Integer]
97
+ def heredoc_indent_level(node)
98
+ dstr_node = node.child_nodes[1]
99
+ return 0 unless dstr_node&.dstr_type?
100
+
101
+ heredoc_indent_type = heredoc_indent_type(node)
102
+ return 0 unless heredoc_indent_type == "~"
103
+
104
+ heredoc_body = dstr_node.loc.heredoc_body.source
105
+ indent_level(heredoc_body)
106
+ end
107
+
108
+ # @param str [String]
109
+ # @return [Integer]
110
+ # @see https://github.com/rubocop/rubocop/blob/v1.21.0/lib/rubocop/cop/mixin/heredoc.rb#L23-L28
111
+ def indent_level(str)
112
+ indentations = str.lines.
113
+ map { |line| line[/^\s*/] }.
114
+ reject { |line| line.end_with?("\n") }
115
+ indentations.empty? ? 0 : indentations.min_by(&:size).size
116
+ end
117
+
118
+ # Returns '~', '-' or nil
119
+ #
120
+ # @param node [RuboCop::AST::Node]
121
+ # @return [String,nil] '~', '-' or `nil`
122
+ # @see https://github.com/rubocop/rubocop/blob/v1.21.0/lib/rubocop/cop/layout/heredoc_indentation.rb#L146-L149
123
+ def heredoc_indent_type(node)
124
+ node.source[/<<([~-])/, 1]
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Isucon
6
+ module Mixin
7
+ # Common methods for {RuboCop::Cop::Isucon::Mysql2::SelectAsterisk} and {RuboCop::Cop::Isucon::Sqlite3::SelectAsterisk}
8
+ module SelectAsteriskMethods
9
+ include Mixin::DatabaseMethods
10
+
11
+ MSG = "Use SELECT with column names. (e.g. `SELECT id, name FROM table_name`)"
12
+
13
+ TODO = "# TODO: Remove needless columns if necessary\n"
14
+
15
+ # @param node [RuboCop::AST::Node]
16
+ def on_send(node)
17
+ with_error_handling(node) do
18
+ with_db_query(node) do |type, root_gda|
19
+ check_and_register_offence(type: type, root_gda: root_gda, node: node)
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
27
+ # @param root_gda [RuboCop::Isucon::GDA::Client]
28
+ # @param node [RuboCop::AST::Node]
29
+ def check_and_register_offence(type:, root_gda:, node:)
30
+ return unless root_gda
31
+
32
+ root_gda.visit_all do |gda|
33
+ next unless gda.ast.respond_to?(:expr_list)
34
+
35
+ gda.ast.expr_list.each do |select_field_node|
36
+ check_and_register_offence_for_select_field_node(
37
+ type: type, node: node, gda: gda,
38
+ select_field_node: select_field_node
39
+ )
40
+ end
41
+ end
42
+ end
43
+
44
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
45
+ # @param node [RuboCop::AST::Node]
46
+ # @param gda [RuboCop::Isucon::GDA::Client]
47
+ # @param select_field_node [GDA::Nodes::SelectField]
48
+ def check_and_register_offence_for_select_field_node(type:, node:, gda:, select_field_node:)
49
+ return unless select_field_node.respond_to?(:expr)
50
+
51
+ select_field = parse_select_field_node(select_field_node)
52
+
53
+ return unless select_field[:column_name] == "*"
54
+
55
+ loc = offense_location(type: type, node: node, gda_location: select_field_node.expr.location)
56
+ return unless loc
57
+
58
+ add_offense(loc) do |corrector|
59
+ perform_autocorrect(corrector: corrector, loc: loc, gda: gda, node: node,
60
+ select_table_name: select_field[:table_name])
61
+ end
62
+ end
63
+
64
+ # @param select_field_node [GDA::Nodes::SelectField]
65
+ # @return [Hash<Symbol, String>] table_name, column_name
66
+ def parse_select_field_node(select_field_node)
67
+ column_elements = select_field_node.expr.value.split(".", 2)
68
+
69
+ case column_elements.count
70
+ when 1
71
+ return { column_name: column_elements[0] }
72
+ when 2
73
+ return { table_name: column_elements[0], column_name: column_elements[1] }
74
+ end
75
+
76
+ {}
77
+ end
78
+
79
+ # @param corrector [RuboCop::Cop::Corrector]
80
+ # @param loc [Parser::Source::Range]
81
+ # @param gda [RuboCop::Isucon::GDA::Client]
82
+ # @param node [RuboCop::AST::Node]
83
+ # @param select_table_name [String,nil] table names included in the SELECT clause
84
+ def perform_autocorrect(corrector:, loc:, gda:, node:, select_table_name:)
85
+ return unless enabled_database?
86
+ return if gda.table_names.empty?
87
+
88
+ if select_table_name
89
+ return unless gda.table_names.include?(select_table_name)
90
+
91
+ replace_asterisk(corrector: corrector, loc: loc, table_name: select_table_name, table_prefix: true)
92
+ else
93
+ return unless gda.table_names.length == 1
94
+
95
+ replace_asterisk(corrector: corrector, loc: loc, table_name: gda.table_names[0], table_prefix: false)
96
+ end
97
+
98
+ insert_todo_comment(corrector: corrector, node: node)
99
+ end
100
+
101
+ # @param corrector [RuboCop::Cop::Corrector]
102
+ # @param loc [Parser::Source::Range]
103
+ # @param table_name [String]
104
+ # @param table_prefix [Boolean] Whether add table name to prefix (e.g. `users`.`name`)
105
+ def replace_asterisk(corrector:, loc:, table_name:, table_prefix:)
106
+ select_columns = columns_in_select_clause(table_name: table_name, table_prefix: table_prefix)
107
+ corrector.replace(loc, select_columns)
108
+ end
109
+
110
+ # @param table_name [String]
111
+ # @param table_prefix [Boolean] Whether add table name to prefix (e.g. `users`.`name`)
112
+ # @return [String]
113
+ def columns_in_select_clause(table_name:, table_prefix:)
114
+ column_names = connection.column_names(table_name)
115
+
116
+ column_names.map do |column|
117
+ if table_prefix
118
+ "`#{table_name}`.`#{column}`"
119
+ else
120
+ "`#{column}`"
121
+ end
122
+ end.join(", ")
123
+ end
124
+
125
+ # @param corrector [RuboCop::Cop::Corrector]
126
+ # @param node [RuboCop::AST::Node]
127
+ def insert_todo_comment(corrector:, node:)
128
+ current_line = node.loc.expression.line
129
+ current_line_range = node.loc.expression.source_buffer.line_range(current_line)
130
+
131
+ indent = node_indent_level(node)
132
+ comment_line = (" " * indent) + TODO
133
+ corrector.insert_before(current_line_range, comment_line)
134
+ end
135
+
136
+ # @param node [RuboCop::AST::Node]
137
+ # @return [Integer]
138
+ def node_indent_level(node)
139
+ node.loc.expression.source_line =~ /^(\s+)/
140
+ return 0 unless Regexp.last_match(1)
141
+
142
+ Regexp.last_match(1).length
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Isucon
6
+ module Mixin
7
+ # Helper methods for `db.execute` in AST
8
+ module Sqlite3ExecuteMethods
9
+ extend NodePattern::Macros
10
+
11
+ include OffenceLocationMethods
12
+
13
+ # @!method find_xquery(node)
14
+ # @param node [RuboCop::AST::Node]
15
+ def_node_search :find_execute, <<~PATTERN
16
+ (send _ {:execute | :get_first_row} (${str dstr lvar ivar cvar} $...) ...)
17
+ PATTERN
18
+
19
+ NON_STRING_WARNING_MSG = "Warning: non-string was passed to `execute` or `get_first_row` 1st argument. " \
20
+ "So argument doesn't parsed as SQL (%<file_path>s:%<line_num>d)"
21
+
22
+ # @param node [RuboCop::AST::Node]
23
+ # @yieldparam type [Symbol] Node type. one of `:str`, `:dstr`
24
+ # @yieldparam root_gda [RuboCop::Isucon::GDA::Client,nil]
25
+ #
26
+ # @note If arguments of `db.xquery` isn't string, `root_gda` is `nil`
27
+ def with_db_query(node)
28
+ find_execute(node) do |type, params|
29
+ sql = execute_param(type: type, params: params)
30
+
31
+ unless sql
32
+ warn format(NON_STRING_WARNING_MSG, file_path: processed_source.file_path, line_num: node.loc.expression.line)
33
+ end
34
+
35
+ root_gda = sql ? RuboCop::Isucon::GDA::Client.new(sql) : nil
36
+
37
+ yield type, root_gda
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # @return [Array<Symbol>]
44
+ def db_query_methods
45
+ %i[execute get_first_row]
46
+ end
47
+
48
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
49
+ # @param params [Array<RuboCop::AST::Node>]
50
+ # @return [String,nil]
51
+ def execute_param(type:, params:)
52
+ case type
53
+ when :str
54
+ return params[0]
55
+ when :dstr
56
+ if params.all? { |param| param.respond_to?(:value) }
57
+ # heredoc
58
+ return params.map(&:value).join
59
+ end
60
+ end
61
+ nil
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Isucon
6
+ module Mixin
7
+ # Common methods for {RuboCop::Cop::Isucon::Mysql2::WhereWithoutIndex}
8
+ # and {RuboCop::Cop::Isucon::Sqlite3::WhereWithoutIndex}
9
+ module WhereWithoutIndexMethods
10
+ include Mixin::DatabaseMethods
11
+
12
+ # @param node [RuboCop::AST::Node]
13
+ def on_send(node)
14
+ with_error_handling(node) do
15
+ return unless enabled_database?
16
+
17
+ with_db_query(node) do |type, root_gda|
18
+ check_and_register_offence(type: type, root_gda: root_gda, node: node)
19
+ end
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
26
+ # @param root_gda [RuboCop::Isucon::GDA::Client]
27
+ # @param node [RuboCop::AST::Node]
28
+ def check_and_register_offence(type:, root_gda:, node:)
29
+ return unless root_gda
30
+ return if exists_index_in_where_clause_columns?(root_gda)
31
+
32
+ register_offense(type: type, node: node, root_gda: root_gda)
33
+ end
34
+
35
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
36
+ # @param node [RuboCop::AST::Node]
37
+ # @param root_gda [RuboCop::Isucon::GDA::Client]
38
+ def register_offense(type:, node:, root_gda:)
39
+ root_gda.visit_all do |gda|
40
+ next if gda.where_nodes.empty?
41
+
42
+ loc = offense_location(type: type, node: node, gda_location: gda.where_nodes.first.location)
43
+ next unless loc
44
+
45
+ message = offense_message(gda)
46
+ add_offense(loc, message: message)
47
+ end
48
+ end
49
+
50
+ # @param gda [RuboCop::Isucon::GDA::Client]
51
+ def offense_message(gda)
52
+ column_name = gda.where_conditions[0].column_operand
53
+ table_name = find_table_name_from_column_name(table_names: gda.table_names, column_name: column_name)
54
+ generate_offense_message(table_name: table_name, column_name: column_name)
55
+ end
56
+
57
+ # @param root_gda [RuboCop::Isucon::GDA::Client]
58
+ # @return [Boolean]
59
+ def exists_index_in_where_clause_columns?(root_gda)
60
+ root_gda.visit_all do |gda|
61
+ gda.table_names.each do |table_name|
62
+ return true if covered_where_column_in_index?(gda: gda, table_name: table_name)
63
+ return true if covered_where_column_in_primary_key?(gda: gda, table_name: table_name)
64
+ end
65
+ end
66
+
67
+ false
68
+ end
69
+
70
+ # @param gda [RuboCop::Isucon::GDA::Client]
71
+ # @param table_name [String]
72
+ # @return [Boolean]
73
+ def covered_where_column_in_index?(gda:, table_name:)
74
+ indexes = connection.indexes(table_name)
75
+ index_first_columns = indexes.map { |index| index.columns[0] }
76
+
77
+ gda.where_conditions.any? do |condition|
78
+ index_first_columns.include?(condition.column_operand)
79
+ end
80
+ end
81
+
82
+ # @param gda [RuboCop::Isucon::GDA::Client]
83
+ # @param table_name [String]
84
+ # @return [Boolean]
85
+ def covered_where_column_in_primary_key?(gda:, table_name:)
86
+ primary_keys = connection.primary_keys(table_name)
87
+ return false if primary_keys.empty?
88
+
89
+ where_columns = gda.where_conditions.map(&:column_operand)
90
+ primary_keys.all? { |primary_key| where_columns.include?(primary_key) }
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -16,82 +16,19 @@ module RuboCop
16
16
  # db.xquery('SELECT id, title FROM articles JOIN users ON users.id = articles.user_id')
17
17
  #
18
18
  class JoinWithoutIndex < Base
19
- include Mixin::DatabaseMethods
20
19
  include Mixin::Mysql2XqueryMethods
20
+ include Mixin::JoinWithoutIndexMethods
21
21
 
22
22
  MSG = "This join clause doesn't seem to have an index. " \
23
23
  "(e.g. `ALTER TABLE %<table_name>s ADD INDEX index_%<column_name>s (%<column_name>s)`)"
24
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
25
  private
37
26
 
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
27
  # @param table_name [String]
69
28
  # @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)
29
+ # @return [String]
30
+ def generate_offense_message(table_name:, column_name:)
31
+ format(MSG, table_name: table_name, column_name: column_name)
95
32
  end
96
33
  end
97
34
  end
@@ -53,32 +53,7 @@ module RuboCop
53
53
  #
54
54
  class ManyJoinTable < Base
55
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
56
+ include Mixin::ManyJoinTableMethods
82
57
  end
83
58
  end
84
59
  end
@@ -51,126 +51,16 @@ module RuboCop
51
51
  class NPlusOneQuery < Base
52
52
  # rubocop:enable Layout/LineLength
53
53
 
54
- include Mixin::DatabaseMethods
55
54
  include Mixin::Mysql2XqueryMethods
55
+ include Mixin::NPlusOneQueryMethods
56
56
 
57
57
  extend AutoCorrector
58
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
59
  private
114
60
 
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
61
+ # [Boolean]
62
+ def array_arg?
63
+ false
174
64
  end
175
65
  end
176
66
  end