rubocop-isucon 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/gh-pages.yml +44 -0
- data/.github/workflows/test.yml +91 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +43 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +108 -0
- data/Rakefile +35 -0
- data/benchmark/README.md +69 -0
- data/benchmark/memorize.rb +86 -0
- data/benchmark/parse_table.rb +103 -0
- data/benchmark/shell.rb +26 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/config/default.yml +83 -0
- data/config/enable-only-performance.yml +30 -0
- data/gemfiles/activerecord_6_1.gemfile +14 -0
- data/gemfiles/activerecord_7_0.gemfile +14 -0
- data/lib/rubocop/cop/isucon/correctors/mysql2_n_plus_one_query_corrector/correctable_methods.rb +66 -0
- data/lib/rubocop/cop/isucon/correctors/mysql2_n_plus_one_query_corrector/replace_methods.rb +127 -0
- data/lib/rubocop/cop/isucon/correctors/mysql2_n_plus_one_query_corrector.rb +112 -0
- data/lib/rubocop/cop/isucon/mixin/database_methods.rb +59 -0
- data/lib/rubocop/cop/isucon/mixin/mysql2_xquery_methods.rb +176 -0
- data/lib/rubocop/cop/isucon/mixin/sinatra_methods.rb +37 -0
- data/lib/rubocop/cop/isucon/mysql2/join_without_index.rb +100 -0
- data/lib/rubocop/cop/isucon/mysql2/many_join_table.rb +86 -0
- data/lib/rubocop/cop/isucon/mysql2/n_plus_one_query.rb +179 -0
- data/lib/rubocop/cop/isucon/mysql2/prepare_execute.rb +136 -0
- data/lib/rubocop/cop/isucon/mysql2/select_asterisk.rb +171 -0
- data/lib/rubocop/cop/isucon/mysql2/where_without_index.rb +105 -0
- data/lib/rubocop/cop/isucon/shell/backtick.rb +36 -0
- data/lib/rubocop/cop/isucon/shell/system.rb +36 -0
- data/lib/rubocop/cop/isucon/sinatra/disable_logging.rb +83 -0
- data/lib/rubocop/cop/isucon/sinatra/logger.rb +52 -0
- data/lib/rubocop/cop/isucon/sinatra/rack_logger.rb +58 -0
- data/lib/rubocop/cop/isucon/sinatra/serve_static_file.rb +73 -0
- data/lib/rubocop/cop/isucon_cops.rb +20 -0
- data/lib/rubocop/isucon/database_connection.rb +42 -0
- data/lib/rubocop/isucon/gda/client.rb +184 -0
- data/lib/rubocop/isucon/gda/gda_ext.rb +119 -0
- data/lib/rubocop/isucon/gda/join_condition.rb +25 -0
- data/lib/rubocop/isucon/gda/join_operand.rb +46 -0
- data/lib/rubocop/isucon/gda/node_location.rb +42 -0
- data/lib/rubocop/isucon/gda/node_patcher.rb +101 -0
- data/lib/rubocop/isucon/gda/where_condition.rb +73 -0
- data/lib/rubocop/isucon/gda/where_operand.rb +32 -0
- data/lib/rubocop/isucon/gda.rb +28 -0
- data/lib/rubocop/isucon/inject.rb +20 -0
- data/lib/rubocop/isucon/memorize_methods.rb +38 -0
- data/lib/rubocop/isucon/version.rb +7 -0
- data/lib/rubocop/isucon.rb +20 -0
- data/lib/rubocop-isucon.rb +16 -0
- data/rubocop-isucon.gemspec +52 -0
- metadata +286 -0
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "rubocop/isucon"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/config/default.yml
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
Isucon/Mysql2:
|
2
|
+
StyleGuideBaseURL: https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Mysql2/
|
3
|
+
Database:
|
4
|
+
|
5
|
+
Isucon/Mysql2/JoinWithoutIndex:
|
6
|
+
Description: 'Check for `JOIN` without index'
|
7
|
+
Enabled: true
|
8
|
+
VersionAdded: '0.1.0'
|
9
|
+
StyleGuide: JoinWithoutIndex.html
|
10
|
+
|
11
|
+
Isucon/Mysql2/ManyJoinTable:
|
12
|
+
Description: 'Check if SQL contains many JOINs'
|
13
|
+
Enabled: true
|
14
|
+
VersionAdded: '0.1.0'
|
15
|
+
StyleGuide: ManyJoinTable.html
|
16
|
+
CountTables: 3
|
17
|
+
|
18
|
+
Isucon/Mysql2/NPlusOneQuery:
|
19
|
+
Description: 'Checks that N+1 query is not used'
|
20
|
+
Enabled: true
|
21
|
+
VersionAdded: '0.1.0'
|
22
|
+
SafeAutoCorrect: false
|
23
|
+
StyleGuide: NPlusOneQuery.html
|
24
|
+
|
25
|
+
Isucon/Mysql2/PrepareExecute:
|
26
|
+
Description: 'Use `db.xquery` instead of `db.prepare`'
|
27
|
+
Enabled: true
|
28
|
+
VersionAdded: '0.1.0'
|
29
|
+
StyleGuide: PrepareExecute.html
|
30
|
+
|
31
|
+
Isucon/Mysql2/SelectAsterisk:
|
32
|
+
Description: 'Avoid `SELECT *` in `db.xquery`'
|
33
|
+
Enabled: true
|
34
|
+
VersionAdded: '0.1.0'
|
35
|
+
StyleGuide: SelectAsterisk.html
|
36
|
+
|
37
|
+
Isucon/Mysql2/WhereWithoutIndex:
|
38
|
+
Description: 'Check for `WHERE` without index'
|
39
|
+
Enabled: true
|
40
|
+
VersionAdded: '0.1.0'
|
41
|
+
StyleGuide: WhereWithoutIndex.html
|
42
|
+
|
43
|
+
Isucon/Shell:
|
44
|
+
StyleGuideBaseURL: https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Shell/
|
45
|
+
|
46
|
+
Isucon/Shell/Backtick:
|
47
|
+
Description: 'Avoid external command calls with backtick'
|
48
|
+
Enabled: true
|
49
|
+
VersionAdded: '0.1.0'
|
50
|
+
StyleGuide: Backtick.html
|
51
|
+
|
52
|
+
Isucon/Shell/System:
|
53
|
+
Description: 'Avoid external command calls with `Kernel#system`'
|
54
|
+
Enabled: true
|
55
|
+
VersionAdded: '0.1.0'
|
56
|
+
StyleGuide: System.html
|
57
|
+
|
58
|
+
Isucon/Sinatra:
|
59
|
+
StyleGuideBaseURL: https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Sinatra/
|
60
|
+
|
61
|
+
Isucon/Sinatra/DisableLogging:
|
62
|
+
Description: 'Disable sinatra logging'
|
63
|
+
Enabled: true
|
64
|
+
VersionAdded: '0.1.0'
|
65
|
+
StyleGuide: DisableLogging.html
|
66
|
+
|
67
|
+
Isucon/Sinatra/Logger:
|
68
|
+
Description: "Disable sinatra logger logging"
|
69
|
+
Enabled: true
|
70
|
+
VersionAdded: '0.1.0'
|
71
|
+
StyleGuide: Logger.html
|
72
|
+
|
73
|
+
Isucon/Sinatra/RackLogger:
|
74
|
+
Description: "Disable `request.env['rack.logger']` logging"
|
75
|
+
Enabled: true
|
76
|
+
VersionAdded: '0.1.0'
|
77
|
+
StyleGuide: RackLogger.html
|
78
|
+
|
79
|
+
Isucon/Sinatra/ServeStaticFile:
|
80
|
+
Description: 'Serve static files on front server (e.g. nginx) instead of sinatra app'
|
81
|
+
Enabled: true
|
82
|
+
VersionAdded: '0.1.0'
|
83
|
+
StyleGuide: ServeStaticFile.html
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-performance
|
3
|
+
|
4
|
+
# Disable default cops (except Performance cops)
|
5
|
+
Bundler:
|
6
|
+
Enabled: false
|
7
|
+
|
8
|
+
Gemspec:
|
9
|
+
Enabled: false
|
10
|
+
|
11
|
+
Layout:
|
12
|
+
Enabled: false
|
13
|
+
|
14
|
+
Lint:
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
Metrics:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
Naming:
|
21
|
+
Enabled: false
|
22
|
+
|
23
|
+
Performance:
|
24
|
+
Enabled: true
|
25
|
+
|
26
|
+
Security:
|
27
|
+
Enabled: false
|
28
|
+
|
29
|
+
Style:
|
30
|
+
Enabled: false
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "activerecord", "~> 6.1.0"
|
6
|
+
|
7
|
+
group :sqlite3 do
|
8
|
+
# c.f. https://github.com/rails/rails/blob/v6.1.0/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L13
|
9
|
+
gem "sqlite3", "~> 1.4"
|
10
|
+
end
|
11
|
+
|
12
|
+
# eval_gemfile "#{__dir__}/common.gemfile"
|
13
|
+
|
14
|
+
gemspec path: "../"
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "activerecord", "~> 7.0.1"
|
6
|
+
|
7
|
+
group :sqlite3 do
|
8
|
+
# c.f. https://github.com/rails/rails/blob/v7.0.1/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L13
|
9
|
+
gem "sqlite3", "~> 1.4"
|
10
|
+
end
|
11
|
+
|
12
|
+
# eval_gemfile "#{__dir__}/common.gemfile"
|
13
|
+
|
14
|
+
gemspec path: "../"
|
data/lib/rubocop/cop/isucon/correctors/mysql2_n_plus_one_query_corrector/correctable_methods.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Isucon
|
6
|
+
module Correctors
|
7
|
+
class Mysql2NPlusOneQueryCorrector
|
8
|
+
# Check whether can correct
|
9
|
+
module CorrectableMethods
|
10
|
+
# @return [Boolean]
|
11
|
+
def correctable?
|
12
|
+
correctable_gda? && correctable_xquery_arg? &&
|
13
|
+
correctable_parent_receiver? && current_node.child_nodes.count == 3 &&
|
14
|
+
xquery_lvar.lvasgn_type? && %i[first last].include?(xquery_chained_method)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Boolean]
|
18
|
+
def correctable_gda?
|
19
|
+
gda&.select_query? && gda.table_names.count == 1 && !gda.limit_clause? &&
|
20
|
+
!gda.group_by_clause? && !gda.contains_aggregate_functions? && where_clause_with_only_single_unique_key?
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [Boolean]
|
24
|
+
def where_clause_with_only_single_unique_key?
|
25
|
+
where_clause_with_only_primary_key? || where_clause_with_only_single_unique_index_column?
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [Boolean]
|
29
|
+
def where_clause_with_only_primary_key?
|
30
|
+
return false unless gda.where_nodes.count == 1
|
31
|
+
|
32
|
+
primary_keys = connection.primary_keys(gda.table_names[0])
|
33
|
+
return false unless primary_keys.count == 1
|
34
|
+
|
35
|
+
primary_keys.first == where_column_without_quote
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [Boolean]
|
39
|
+
def where_clause_with_only_single_unique_index_column?
|
40
|
+
return false unless gda.where_nodes.count == 1
|
41
|
+
|
42
|
+
unique_index_columns = connection.unique_index_columns(gda.table_names[0])
|
43
|
+
unique_index_columns.any? { |columns| columns.count == 1 && columns.first == where_column_without_quote }
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Boolean]
|
47
|
+
def correctable_xquery_arg? # rubocop:disable Metrics/AbcSize
|
48
|
+
return false if !xquery_arg&.send_type? || xquery_arg.node_parts.count != 3 || !xquery_arg.node_parts[0].lvar_type?
|
49
|
+
|
50
|
+
# Check one of hash[:key], hash["key"], hash.fetch(:key), hash.fetch("key")
|
51
|
+
return false unless %i[[] fetch].include?(xquery_arg.node_parts[1])
|
52
|
+
return false unless %i[sym str].include?(xquery_arg.node_parts[2].type)
|
53
|
+
|
54
|
+
true
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Boolean]
|
58
|
+
def correctable_parent_receiver?
|
59
|
+
parent_receiver.lvar_type? || parent_receiver.send_type?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Isucon
|
6
|
+
module Correctors
|
7
|
+
class Mysql2NPlusOneQueryCorrector
|
8
|
+
# replace ast
|
9
|
+
module ReplaceMethods
|
10
|
+
def replace
|
11
|
+
replace_where_condition_in_sql
|
12
|
+
replace_xquery_2nd_arg
|
13
|
+
replace_chained_method_to_each_with_object
|
14
|
+
replace_to_2_lines
|
15
|
+
end
|
16
|
+
|
17
|
+
# Replace where condition in SQL (e.g. `id = ?` -> `id IN (?)`)
|
18
|
+
def replace_where_condition_in_sql
|
19
|
+
loc = offense_location(type: type, node: current_node, gda_location: where_condition_gda_loc)
|
20
|
+
return unless loc
|
21
|
+
|
22
|
+
corrector.replace(loc, "#{where_column} IN (?)")
|
23
|
+
end
|
24
|
+
|
25
|
+
# Replace 2nd arg in db.xquery (e.g. `course[:teacher_id]` -> `courses.map { |course| course[:teacher_id] }`)
|
26
|
+
def replace_xquery_2nd_arg
|
27
|
+
object_source = xquery_arg.node_parts[0].source
|
28
|
+
symbol_source = xquery_arg.node_parts[2].source
|
29
|
+
|
30
|
+
corrector.replace(
|
31
|
+
xquery_arg.loc.expression,
|
32
|
+
# e.g.
|
33
|
+
# courses.map { |course| course[:teacher_id] }
|
34
|
+
"#{parent_receiver.source}.map { |#{object_source}| #{object_source}[#{symbol_source}] }",
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Replace `.first` -> `.each_with_object({}) { |v, hash| hash[v[:id]] = v }`
|
39
|
+
def replace_chained_method_to_each_with_object
|
40
|
+
xquery_chained_method_begin_pos = current_node.loc.end.end_pos + 1
|
41
|
+
xquery_chained_method_range =
|
42
|
+
Parser::Source::Range.new(current_node.loc.expression.source_buffer,
|
43
|
+
xquery_chained_method_begin_pos,
|
44
|
+
xquery_chained_method_begin_pos + xquery_chained_method.length)
|
45
|
+
|
46
|
+
corrector.replace(xquery_chained_method_range, generate_each_with_object)
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [String]
|
50
|
+
#
|
51
|
+
# @example response example
|
52
|
+
# each_with_object({}) { |v, hash| hash[v[:id]] = v }
|
53
|
+
def generate_each_with_object
|
54
|
+
hash_key =
|
55
|
+
case xquery_arg.node_parts[2].type
|
56
|
+
when :sym
|
57
|
+
":#{where_column_without_quote}"
|
58
|
+
when :str
|
59
|
+
%("#{where_column_without_quote}")
|
60
|
+
end
|
61
|
+
|
62
|
+
"each_with_object({}) { |v, hash| hash[v[#{hash_key}]] = v }"
|
63
|
+
end
|
64
|
+
|
65
|
+
# rubocop:disable Layout/LineLength
|
66
|
+
|
67
|
+
# Split line
|
68
|
+
#
|
69
|
+
# @example Before
|
70
|
+
# teacher = db.xquery("SELECT * FROM `users` WHERE `id` IN (?)", courses.map { |course| course[:teacher_id] }).each_with_object({}) { |v, hash| hash[v[:id]] = v }
|
71
|
+
#
|
72
|
+
# @example After
|
73
|
+
# @users_by_id ||= db.xquery("SELECT * FROM `users` WHERE `id` IN (?)", ...).each_with_object({}) { |v, hash| hash[v[:id]] = v }
|
74
|
+
# teacher = @users_by_id[course[:teacher_id]]
|
75
|
+
def replace_to_2_lines
|
76
|
+
# rubocop:enable Layout/LineLength
|
77
|
+
|
78
|
+
replace_to_2_lines_for_1st_line
|
79
|
+
replace_to_2_lines_for_2nd_line
|
80
|
+
end
|
81
|
+
|
82
|
+
def replace_to_2_lines_for_1st_line
|
83
|
+
range =
|
84
|
+
Parser::Source::Range.new(current_node.loc.expression.source_buffer,
|
85
|
+
xquery_lvar.loc.expression.begin_pos, current_node.loc.expression.begin_pos)
|
86
|
+
|
87
|
+
corrector.replace(range, "#{instance_var_name} ||= ")
|
88
|
+
end
|
89
|
+
|
90
|
+
# @return [String]
|
91
|
+
def instance_var_name
|
92
|
+
"@#{gda.table_names[0]}_by_#{where_column_without_quote}"
|
93
|
+
end
|
94
|
+
|
95
|
+
def replace_to_2_lines_for_2nd_line
|
96
|
+
indent_level = indent_level(current_node)
|
97
|
+
|
98
|
+
pos = xquery_lvar.loc.expression.end_pos + 1
|
99
|
+
range = Parser::Source::Range.new(current_node.loc.expression.source_buffer, pos, pos)
|
100
|
+
|
101
|
+
corrector.replace(range, "#{' ' * indent_level}#{generate_second_line}")
|
102
|
+
end
|
103
|
+
|
104
|
+
# @param node [RuboCop::AST::Node]
|
105
|
+
# @return [Integer]
|
106
|
+
def indent_level(node)
|
107
|
+
node.loc.expression.source_line =~ /^(\s+)/
|
108
|
+
return 0 unless Regexp.last_match(1)
|
109
|
+
|
110
|
+
Regexp.last_match(1).length
|
111
|
+
end
|
112
|
+
|
113
|
+
# @return [String]
|
114
|
+
#
|
115
|
+
# @example response example
|
116
|
+
# teacher = @users_by_id[course[:teacher_id]]
|
117
|
+
def generate_second_line
|
118
|
+
object_source = xquery_arg.node_parts[0].source
|
119
|
+
symbol_source = xquery_arg.node_parts[2].source
|
120
|
+
"#{xquery_lvar.node_parts[0]} = #{instance_var_name}[#{object_source}[#{symbol_source}]]\n"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "mysql2_n_plus_one_query_corrector/correctable_methods"
|
4
|
+
require_relative "mysql2_n_plus_one_query_corrector/replace_methods"
|
5
|
+
|
6
|
+
module RuboCop
|
7
|
+
module Cop
|
8
|
+
module Isucon
|
9
|
+
module Correctors
|
10
|
+
# rubocop:disable Layout/LineLength
|
11
|
+
|
12
|
+
# Corrector for {RuboCop::Cop::Isucon::Mysql2::NPlusOneQuery}
|
13
|
+
#
|
14
|
+
# @example Before
|
15
|
+
# courses.map do |course|
|
16
|
+
# teacher = db.xquery('SELECT * FROM `users` WHERE `id` = ?', course[:teacher_id]).first
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# @example After
|
20
|
+
# courses.map do |course|
|
21
|
+
# @users_by_id ||= db.xquery('SELECT * FROM `users` WHERE `id` IN (?)', courses.map { |course| course[:teacher_id] }).each_with_object({}) { |v, hash| hash[v[:id]] = v }
|
22
|
+
# teacher = @users_by_id[course[:teacher_id]]
|
23
|
+
# end
|
24
|
+
class Mysql2NPlusOneQueryCorrector
|
25
|
+
# rubocop:enable Layout/LineLength
|
26
|
+
|
27
|
+
include Mixin::Mysql2XqueryMethods
|
28
|
+
include CorrectableMethods
|
29
|
+
include ReplaceMethods
|
30
|
+
|
31
|
+
# @return [RuboCop::Cop::Corrector]
|
32
|
+
attr_reader :corrector
|
33
|
+
|
34
|
+
# @return [RuboCop::AST::Node]
|
35
|
+
attr_reader :current_node
|
36
|
+
|
37
|
+
# @return [RuboCop::AST::Node]
|
38
|
+
attr_reader :parent_node
|
39
|
+
|
40
|
+
# @return [Symbol]
|
41
|
+
attr_reader :type
|
42
|
+
|
43
|
+
# @return [RuboCop::Isucon::GDA::Client]
|
44
|
+
attr_reader :gda
|
45
|
+
|
46
|
+
# @return [RuboCop::Isucon::DatabaseConnection]
|
47
|
+
attr_reader :connection
|
48
|
+
|
49
|
+
# @param corrector [RuboCop::Cop::Corrector]
|
50
|
+
# @param current_node [RuboCop::AST::Node]
|
51
|
+
# @param parent_node [RuboCop::AST::Node]
|
52
|
+
# @param type [Symbol] Node type. one of `:str`, `:dstr`
|
53
|
+
# @param gda [RuboCop::Isucon::GDA::Client]
|
54
|
+
# @param connection [RuboCop::Isucon::DatabaseConnection]
|
55
|
+
def initialize(corrector:, current_node:, parent_node:, type:, gda:, connection:) # rubocop:disable Metrics/ParameterLists
|
56
|
+
@corrector = corrector
|
57
|
+
@current_node = current_node
|
58
|
+
@parent_node = parent_node
|
59
|
+
@type = type
|
60
|
+
@gda = gda
|
61
|
+
@connection = connection
|
62
|
+
end
|
63
|
+
|
64
|
+
def correct
|
65
|
+
replace if correctable?
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# @return [RuboCop::AST::Node,nil]
|
71
|
+
def parent_receiver
|
72
|
+
parent_node.child_nodes&.first&.receiver
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [RuboCop::Isucon::GDA::NodeLocation]
|
76
|
+
def where_condition_gda_loc
|
77
|
+
gda.where_nodes.first.location
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [String,nil]
|
81
|
+
def where_column
|
82
|
+
matched = where_condition_gda_loc&.body&.match(/([^\s]+)\s*=\s*\?/)
|
83
|
+
|
84
|
+
return nil unless matched
|
85
|
+
|
86
|
+
matched[1]
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [RuboCop::AST::Node]
|
90
|
+
def xquery_arg
|
91
|
+
current_node.child_nodes[2]
|
92
|
+
end
|
93
|
+
|
94
|
+
# @return [RuboCop::AST::Node]
|
95
|
+
def xquery_chained_method
|
96
|
+
current_node.parent.node_parts[1]
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [String]
|
100
|
+
def where_column_without_quote
|
101
|
+
where_column&.delete("`")
|
102
|
+
end
|
103
|
+
|
104
|
+
# @return [RuboCop::AST::Node,nil]
|
105
|
+
def xquery_lvar
|
106
|
+
current_node.parent&.parent
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Isucon
|
6
|
+
module Mixin
|
7
|
+
# Database util methods for {RuboCop::Cop::Isucon::Mysql2}
|
8
|
+
module DatabaseMethods
|
9
|
+
# @return [RuboCop::Isucon::DatabaseConnection]
|
10
|
+
# @raise [RuboCop::Isucon::DatabaseConfigurationError] `Database` isn't configured in `.rubocop.yml`
|
11
|
+
def connection
|
12
|
+
return @connection if @connection
|
13
|
+
|
14
|
+
unless enabled_database?
|
15
|
+
raise RuboCop::Isucon::DatabaseConfigurationError, "`Database` isn't configured in `.rubocop.yml`"
|
16
|
+
end
|
17
|
+
|
18
|
+
@connection = RuboCop::Isucon::DatabaseConnection.new(cop_config["Database"])
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [Boolean]
|
22
|
+
def enabled_database?
|
23
|
+
adapter = cop_config.dig("Database", "adapter")
|
24
|
+
adapter && !adapter.empty?
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param table_names [Array<String>]
|
28
|
+
# @param column_name [String]
|
29
|
+
# @return [String,nil]
|
30
|
+
def find_table_name_from_column_name(table_names:, column_name:)
|
31
|
+
table_names.each do |table_name|
|
32
|
+
column_names = connection.column_names(table_name)
|
33
|
+
return table_name if column_names.include?(column_name)
|
34
|
+
end
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# @param node [RuboCop::AST::Node]
|
41
|
+
def with_error_handling(node)
|
42
|
+
yield
|
43
|
+
rescue ActiveRecord::StatementInvalid => e
|
44
|
+
# NOTE: suppress error (e.g. table isn't found in database)
|
45
|
+
print_warning(node: node, error: e)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @param node [RuboCop::AST::Node]
|
49
|
+
# @param error [StandardError]
|
50
|
+
def print_warning(node:, error:)
|
51
|
+
file_path = processed_source.file_path
|
52
|
+
line_num = node.loc.expression.line
|
53
|
+
warn "Warning: #{error.message} (#{file_path}:#{line_num})"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|