rubocop-isucon 0.1.0 → 1.0.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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +10 -0
- data/.github/workflows/pages.yml +68 -0
- data/.github/workflows/test.yml +3 -7
- data/.rubocop.yml +5 -1
- data/CHANGELOG.md +21 -1
- data/README.md +24 -6
- data/config/default.yml +37 -1
- data/gemfiles/activerecord_7_1.gemfile +14 -0
- data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector → n_plus_one_query_corrector}/correctable_methods.rb +1 -1
- data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector → n_plus_one_query_corrector}/replace_methods.rb +1 -1
- data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector.rb → n_plus_one_query_corrector.rb} +16 -5
- data/lib/rubocop/cop/isucon/mixin/join_without_index_methods.rb +87 -0
- data/lib/rubocop/cop/isucon/mixin/many_join_table_methods.rb +39 -0
- data/lib/rubocop/cop/isucon/mixin/mysql2_xquery_methods.rb +7 -116
- data/lib/rubocop/cop/isucon/mixin/n_plus_one_query_methods.rb +153 -0
- data/lib/rubocop/cop/isucon/mixin/offense_location_methods.rb +130 -0
- data/lib/rubocop/cop/isucon/mixin/select_asterisk_methods.rb +148 -0
- data/lib/rubocop/cop/isucon/mixin/sqlite3_execute_methods.rb +67 -0
- data/lib/rubocop/cop/isucon/mixin/where_without_index_methods.rb +96 -0
- data/lib/rubocop/cop/isucon/mysql2/join_without_index.rb +4 -67
- data/lib/rubocop/cop/isucon/mysql2/many_join_table.rb +1 -26
- data/lib/rubocop/cop/isucon/mysql2/n_plus_one_query.rb +5 -115
- data/lib/rubocop/cop/isucon/mysql2/select_asterisk.rb +1 -135
- data/lib/rubocop/cop/isucon/mysql2/where_without_index.rb +7 -72
- data/lib/rubocop/cop/isucon/shell/backtick.rb +7 -0
- data/lib/rubocop/cop/isucon/sqlite3/join_without_index.rb +37 -0
- data/lib/rubocop/cop/isucon/sqlite3/many_join_table.rb +61 -0
- data/lib/rubocop/cop/isucon/sqlite3/n_plus_one_query.rb +70 -0
- data/lib/rubocop/cop/isucon/sqlite3/select_asterisk.rb +37 -0
- data/lib/rubocop/cop/isucon/sqlite3/where_without_index.rb +40 -0
- data/lib/rubocop/cop/isucon_cops.rb +13 -1
- data/lib/rubocop/isucon/gda/client.rb +1 -1
- data/lib/rubocop/isucon/gda/join_operand.rb +1 -1
- data/lib/rubocop/isucon/version.rb +1 -1
- data/rubocop-isucon.gemspec +3 -2
- metadata +38 -10
- data/.github/workflows/gh-pages.yml +0 -44
@@ -0,0 +1,153 @@
|
|
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::NPlusOneQuery}
|
8
|
+
# and {RuboCop::Cop::Isucon::Sqlite3::NPlusOneQuery}
|
9
|
+
module NPlusOneQueryMethods
|
10
|
+
include Mixin::DatabaseMethods
|
11
|
+
|
12
|
+
extend NodePattern::Macros
|
13
|
+
|
14
|
+
MSG = "This looks like N+1 query."
|
15
|
+
|
16
|
+
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L38
|
17
|
+
POST_CONDITION_LOOP_TYPES = %i[while_post until_post].freeze
|
18
|
+
|
19
|
+
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L39
|
20
|
+
LOOP_TYPES = (POST_CONDITION_LOOP_TYPES + %i[while until for]).freeze
|
21
|
+
|
22
|
+
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L41
|
23
|
+
ENUMERABLE_METHOD_NAMES = (Enumerable.instance_methods + [:each]).to_set.freeze
|
24
|
+
|
25
|
+
def_node_matcher :csv_loop?, <<~PATTERN
|
26
|
+
(block
|
27
|
+
(send (const nil? :CSV) :parse ...)
|
28
|
+
...)
|
29
|
+
PATTERN
|
30
|
+
|
31
|
+
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L68
|
32
|
+
def_node_matcher :kernel_loop?, <<~PATTERN
|
33
|
+
(block
|
34
|
+
(send {nil? (const nil? :Kernel)} :loop)
|
35
|
+
...)
|
36
|
+
PATTERN
|
37
|
+
|
38
|
+
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L74
|
39
|
+
def_node_matcher :enumerable_loop?, <<~PATTERN
|
40
|
+
(block
|
41
|
+
(send $_ #enumerable_method? ...)
|
42
|
+
...)
|
43
|
+
PATTERN
|
44
|
+
|
45
|
+
# @param node [RuboCop::AST::Node]
|
46
|
+
def on_send(node)
|
47
|
+
with_error_handling(node) do
|
48
|
+
with_db_query(node) do |type, root_gda|
|
49
|
+
check_and_register_offence(node: node, type: type, root_gda: root_gda, is_array_arg: array_arg?)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
# @param node [RuboCop::AST::Node]
|
57
|
+
# @param type [Symbol] Node type. one of `:str`, `:dstr`
|
58
|
+
# @param root_gda [RuboCop::Isucon::GDA::Client]
|
59
|
+
# @param is_array_arg [Boolean]
|
60
|
+
def check_and_register_offence(node:, type:, root_gda:, is_array_arg:) # rubocop:disable Metrics/MethodLength
|
61
|
+
return unless db_query_node?(node)
|
62
|
+
|
63
|
+
receiver, = *node.children
|
64
|
+
|
65
|
+
parent = parent_loop_node(receiver)
|
66
|
+
return unless parent
|
67
|
+
|
68
|
+
return if or_assignment_to_instance_variable?(node)
|
69
|
+
|
70
|
+
add_offense(receiver) do |corrector|
|
71
|
+
perform_autocorrect(
|
72
|
+
corrector: corrector, current_node: receiver,
|
73
|
+
parent_node: parent, type: type, gda: root_gda, is_array_arg: is_array_arg
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param node [RuboCop::AST::Node]
|
79
|
+
def db_query_node?(node)
|
80
|
+
return db_query_methods.include?(node.children[1]) if node.children.count >= 3
|
81
|
+
|
82
|
+
child = node.children.first
|
83
|
+
return false unless child
|
84
|
+
|
85
|
+
child.children.count >= 3 && db_query_methods.include?(child.children[1])
|
86
|
+
end
|
87
|
+
|
88
|
+
# Whether match to `@instance_var ||=`
|
89
|
+
# @param node [RuboCop::AST::Node]
|
90
|
+
# @return [Boolean]
|
91
|
+
def or_assignment_to_instance_variable?(node)
|
92
|
+
_or_assignment_to_instance_variable?(node.parent&.parent) ||
|
93
|
+
_or_assignment_to_instance_variable?(node.parent&.parent&.parent)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Whether match to `@instance_var ||=`
|
97
|
+
# @param node [RuboCop::AST::Node]
|
98
|
+
# @return [Boolean]
|
99
|
+
def _or_assignment_to_instance_variable?(node)
|
100
|
+
node&.or_asgn_type? && node.child_nodes&.first&.ivasgn_type?
|
101
|
+
end
|
102
|
+
|
103
|
+
# @param node [RuboCop::AST::Node]
|
104
|
+
# @return [RuboCop::AST::Node]
|
105
|
+
def parent_loop_node(node)
|
106
|
+
node.each_ancestor.find { |ancestor| loop?(ancestor, node) }
|
107
|
+
end
|
108
|
+
|
109
|
+
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L106
|
110
|
+
def loop?(ancestor, node)
|
111
|
+
keyword_loop?(ancestor.type) ||
|
112
|
+
kernel_loop?(ancestor) ||
|
113
|
+
node_within_enumerable_loop?(node, ancestor) ||
|
114
|
+
csv_loop?(ancestor)
|
115
|
+
end
|
116
|
+
|
117
|
+
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L112
|
118
|
+
def keyword_loop?(type)
|
119
|
+
LOOP_TYPES.include?(type)
|
120
|
+
end
|
121
|
+
|
122
|
+
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L116
|
123
|
+
def node_within_enumerable_loop?(node, ancestor)
|
124
|
+
enumerable_loop?(ancestor) do |receiver|
|
125
|
+
receiver != node && !receiver&.descendants&.include?(node)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L130
|
130
|
+
def enumerable_method?(method_name)
|
131
|
+
ENUMERABLE_METHOD_NAMES.include?(method_name)
|
132
|
+
end
|
133
|
+
|
134
|
+
# @param corrector [RuboCop::Cop::Corrector]
|
135
|
+
# @param current_node [RuboCop::AST::Node]
|
136
|
+
# @param parent_node [RuboCop::AST::Node]
|
137
|
+
# @param type [Symbol] Node type. one of `:str`, `:dstr`
|
138
|
+
# @param gda [RuboCop::Isucon::GDA::Client]
|
139
|
+
# @param is_array_arg [Boolean]
|
140
|
+
def perform_autocorrect(corrector:, current_node:, parent_node:, type:, gda:, is_array_arg:) # rubocop:disable Metrics/ParameterLists
|
141
|
+
return unless enabled_database?
|
142
|
+
|
143
|
+
corrector = Correctors::NPlusOneQueryCorrector.new(
|
144
|
+
corrector: corrector, current_node: current_node, is_array_arg: is_array_arg,
|
145
|
+
parent_node: parent_node, type: type, gda: gda, connection: connection
|
146
|
+
)
|
147
|
+
corrector.correct
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -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 [
|
71
|
-
def
|
72
|
-
|
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
|