rubocop-isucon 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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