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