rubocop-isucon 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -1
- data/README.md +17 -6
- data/config/default.yml +36 -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} +15 -4
- 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 +4 -114
- data/lib/rubocop/cop/isucon/mysql2/select_asterisk.rb +1 -135
- data/lib/rubocop/cop/isucon/mysql2/where_without_index.rb +5 -70
- 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/version.rb +1 -1
- metadata +17 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 645e1a353538a4c8d8c8914ac76ac7fbb6f1338dbea8d10c16394a6711be725a
|
4
|
+
data.tar.gz: 450b780a05d2e4f5632bd2ffe35453390bfa1e1c4a240662e440b5cbb2e5390c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5c900d876a05963ec323cb6e9a4aec8b6b3b9f35c9bc76910e43c758cf0b6b5203b1d53ee2cf050a7ccfe5f304b4158f6d43a85b72441c42726270e0518b6f83
|
7
|
+
data.tar.gz: 0cea7082761dd2bdc38792b0cd60fbccafa886bded25475fa848fb6c2cbab58ea3a370d5fcbb793ca02402c95aab495bfee8e63b3617f3e5b9420fde4d18070e
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
## [Unreleased]
|
2
|
-
[full changelog](http://github.com/sue445/rubocop-isucon/compare/v0.
|
2
|
+
[full changelog](http://github.com/sue445/rubocop-isucon/compare/v0.2.0...main)
|
3
|
+
|
4
|
+
* Add `Isucon/Sqlite3/SelectAsterisk`
|
5
|
+
* https://github.com/sue445/rubocop-isucon/pull/202
|
6
|
+
* Add `Isucon/Sqlite3/WhereWithoutIndex`
|
7
|
+
* https://github.com/sue445/rubocop-isucon/pull/207
|
8
|
+
* Add `Isucon/Sqlite3/JoinWithoutIndex`
|
9
|
+
* https://github.com/sue445/rubocop-isucon/pull/208
|
10
|
+
* Add `Isucon/Sqlite3/ManyJoinTable`
|
11
|
+
* https://github.com/sue445/rubocop-isucon/pull/209
|
12
|
+
* Add `Isucon/Sqlite3/NPlusOneQuery`
|
13
|
+
* https://github.com/sue445/rubocop-isucon/pull/210
|
14
|
+
|
15
|
+
## [0.1.0] - 2022-07-31
|
16
|
+
[full changelog](http://github.com/sue445/rubocop-isucon/compare/v0.1.0...v0.2.0)
|
3
17
|
|
4
18
|
## [0.1.0] - 2022-07-23
|
5
19
|
|
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# RuboCop ISUCON
|
2
2
|
RuboCop plugin for ruby reference implementation of [ISUCON](https://github.com/isucon)
|
3
3
|
|
4
|
+
[](https://badge.fury.io/rb/rubocop-isucon)
|
4
5
|
[](https://github.com/sue445/rubocop-isucon/actions?query=workflow%3Atest)
|
5
6
|
|
6
7
|
## Installation
|
@@ -72,16 +73,25 @@ Isucon/Mysql2:
|
|
72
73
|
password: isucon
|
73
74
|
encoding: utf8
|
74
75
|
port: 3306
|
76
|
+
|
77
|
+
Isucon/Sqlite3:
|
78
|
+
Database:
|
79
|
+
adapter: sqlite3
|
80
|
+
database: # TODO: Fix this
|
75
81
|
```
|
76
82
|
|
77
83
|
`Database` isn't configured in `.rubocop.yml`, some cops doesn't work
|
78
84
|
|
79
|
-
| cop
|
80
|
-
|
81
|
-
| `Isucon/Mysql2/JoinWithoutIndex`
|
82
|
-
| `Isucon/Mysql2/NPlusOneQuery`
|
83
|
-
| `Isucon/Mysql2/SelectAsterisk`
|
84
|
-
| `Isucon/Mysql2/WhereWithoutIndex`
|
85
|
+
| cop | offense detection | auto-correct |
|
86
|
+
|------------------------------------|----------------------------|----------------------------|
|
87
|
+
| `Isucon/Mysql2/JoinWithoutIndex` | `Database` is **required** | Not supported |
|
88
|
+
| `Isucon/Mysql2/NPlusOneQuery` | `Database` is optional | `Database` is **required** |
|
89
|
+
| `Isucon/Mysql2/SelectAsterisk` | `Database` is optional | `Database` is **required** |
|
90
|
+
| `Isucon/Mysql2/WhereWithoutIndex` | `Database` is **required** | Not supported |
|
91
|
+
| `Isucon/Sqlite3/JoinWithoutIndex` | `Database` is **required** | Not supported |
|
92
|
+
| `Isucon/Sqlite3/NPlusOneQuery` | `Database` is optional | `Database` is **required** |
|
93
|
+
| `Isucon/Sqlite3/SelectAsterisk` | `Database` is optional | `Database` is **required** |
|
94
|
+
| `Isucon/Sqlite3/WhereWithoutIndex` | `Database` is **required** | Not supported |
|
85
95
|
|
86
96
|
## Documentation
|
87
97
|
See. https://sue445.github.io/rubocop-isucon/
|
@@ -89,6 +99,7 @@ See. https://sue445.github.io/rubocop-isucon/
|
|
89
99
|
* `Isucon/Mysql2` department docs : https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Mysql2.html
|
90
100
|
* `Isucon/Shell` department docs : https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Shell.html
|
91
101
|
* `Isucon/Sinatra` department docs : https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Sinatra.html
|
102
|
+
* `Isucon/Sqlite3` department docs : https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Sqlite3.html
|
92
103
|
|
93
104
|
## Benchmark
|
94
105
|
See [benchmark/](benchmark/)
|
data/config/default.yml
CHANGED
@@ -81,3 +81,39 @@ Isucon/Sinatra/ServeStaticFile:
|
|
81
81
|
Enabled: true
|
82
82
|
VersionAdded: '0.1.0'
|
83
83
|
StyleGuide: ServeStaticFile.html
|
84
|
+
|
85
|
+
Isucon/Sqlite3:
|
86
|
+
StyleGuideBaseURL: https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Sqlite3/
|
87
|
+
Database:
|
88
|
+
|
89
|
+
Isucon/Sqlite3/JoinWithoutIndex:
|
90
|
+
Description: 'Check for `JOIN` without index'
|
91
|
+
Enabled: true
|
92
|
+
VersionAdded: '0.2.0'
|
93
|
+
StyleGuide: JoinWithoutIndex.html
|
94
|
+
|
95
|
+
Isucon/Sqlite3/ManyJoinTable:
|
96
|
+
Description: 'Check if SQL contains many JOINs'
|
97
|
+
Enabled: true
|
98
|
+
VersionAdded: '0.2.0'
|
99
|
+
StyleGuide: ManyJoinTable.html
|
100
|
+
CountTables: 3
|
101
|
+
|
102
|
+
Isucon/Sqlite3/NPlusOneQuery:
|
103
|
+
Description: 'Checks that N+1 query is not used'
|
104
|
+
Enabled: true
|
105
|
+
VersionAdded: '0.2.0'
|
106
|
+
SafeAutoCorrect: false
|
107
|
+
StyleGuide: NPlusOneQuery.html
|
108
|
+
|
109
|
+
Isucon/Sqlite3/SelectAsterisk:
|
110
|
+
Description: 'Avoid `SELECT *` in `db.execute`'
|
111
|
+
Enabled: true
|
112
|
+
VersionAdded: '0.2.0'
|
113
|
+
StyleGuide: SelectAsterisk.html
|
114
|
+
|
115
|
+
Isucon/Sqlite3/WhereWithoutIndex:
|
116
|
+
Description: 'Check for `WHERE` without index'
|
117
|
+
Enabled: true
|
118
|
+
VersionAdded: '0.2.0'
|
119
|
+
StyleGuide: WhereWithoutIndex.html
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "
|
4
|
-
require_relative "
|
3
|
+
require_relative "n_plus_one_query_corrector/correctable_methods"
|
4
|
+
require_relative "n_plus_one_query_corrector/replace_methods"
|
5
5
|
|
6
6
|
module RuboCop
|
7
7
|
module Cop
|
@@ -21,7 +21,7 @@ module RuboCop
|
|
21
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
22
|
# teacher = @users_by_id[course[:teacher_id]]
|
23
23
|
# end
|
24
|
-
class
|
24
|
+
class NPlusOneQueryCorrector
|
25
25
|
# rubocop:enable Layout/LineLength
|
26
26
|
|
27
27
|
include Mixin::Mysql2XqueryMethods
|
@@ -46,19 +46,24 @@ module RuboCop
|
|
46
46
|
# @return [RuboCop::Isucon::DatabaseConnection]
|
47
47
|
attr_reader :connection
|
48
48
|
|
49
|
+
# @return [Boolean]
|
50
|
+
attr_reader :is_array_arg
|
51
|
+
|
49
52
|
# @param corrector [RuboCop::Cop::Corrector]
|
50
53
|
# @param current_node [RuboCop::AST::Node]
|
51
54
|
# @param parent_node [RuboCop::AST::Node]
|
52
55
|
# @param type [Symbol] Node type. one of `:str`, `:dstr`
|
53
56
|
# @param gda [RuboCop::Isucon::GDA::Client]
|
54
57
|
# @param connection [RuboCop::Isucon::DatabaseConnection]
|
55
|
-
|
58
|
+
# @param is_array_arg [Boolean]
|
59
|
+
def initialize(corrector:, current_node:, parent_node:, type:, gda:, connection:, is_array_arg:) # rubocop:disable Metrics/ParameterLists
|
56
60
|
@corrector = corrector
|
57
61
|
@current_node = current_node
|
58
62
|
@parent_node = parent_node
|
59
63
|
@type = type
|
60
64
|
@gda = gda
|
61
65
|
@connection = connection
|
66
|
+
@is_array_arg = is_array_arg
|
62
67
|
end
|
63
68
|
|
64
69
|
def correct
|
@@ -67,6 +72,10 @@ module RuboCop
|
|
67
72
|
|
68
73
|
private
|
69
74
|
|
75
|
+
def array_arg?
|
76
|
+
is_array_arg
|
77
|
+
end
|
78
|
+
|
70
79
|
# @return [RuboCop::AST::Node,nil]
|
71
80
|
def parent_receiver
|
72
81
|
parent_node.child_nodes&.first&.receiver
|
@@ -88,6 +97,8 @@ module RuboCop
|
|
88
97
|
|
89
98
|
# @return [RuboCop::AST::Node]
|
90
99
|
def xquery_arg
|
100
|
+
return current_node.child_nodes[2].child_nodes[0] if array_arg? && current_node.child_nodes[2]&.array_type?
|
101
|
+
|
91
102
|
current_node.child_nodes[2]
|
92
103
|
end
|
93
104
|
|
@@ -0,0 +1,87 @@
|
|
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::JoinWithoutIndex}
|
8
|
+
# and {RuboCop::Cop::Isucon::Sqlite3::JoinWithoutIndex}
|
9
|
+
module JoinWithoutIndexMethods
|
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
|
+
|
31
|
+
root_gda.visit_all do |gda|
|
32
|
+
gda.join_conditions.each do |join_condition|
|
33
|
+
join_operand = join_operand_without_index(join_condition)
|
34
|
+
next unless join_operand
|
35
|
+
|
36
|
+
register_offense(type: type, node: node, join_operand: join_operand)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param join_condition [RuboCop::Isucon::GDA::JoinCondition]
|
42
|
+
# @return [RuboCop::Isucon::GDA::JoinOperand,nil]
|
43
|
+
def join_operand_without_index(join_condition)
|
44
|
+
join_condition.operands.each do |join_operand|
|
45
|
+
next unless join_operand.table_name
|
46
|
+
|
47
|
+
unless indexed_column?(table_name: join_operand.table_name, column_name: join_operand.column_name)
|
48
|
+
return join_operand
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param table_name [String]
|
56
|
+
# @param column_name [String]
|
57
|
+
# @return [Boolean]
|
58
|
+
def indexed_column?(table_name:, column_name:)
|
59
|
+
primary_keys = connection.primary_keys(table_name)
|
60
|
+
|
61
|
+
return true if primary_keys&.first == column_name
|
62
|
+
|
63
|
+
indexes = connection.indexes(table_name)
|
64
|
+
index_first_columns = indexes.map { |index| index.columns[0] }
|
65
|
+
index_first_columns.include?(column_name)
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param type [Symbol] Node type. one of `:str`, `:dstr`
|
69
|
+
# @param node [RuboCop::AST::Node]
|
70
|
+
# @param join_operand [RuboCop::Isucon::GDA::JoinOperand]
|
71
|
+
def register_offense(type:, node:, join_operand:)
|
72
|
+
loc = offense_location(type: type, node: node, gda_location: join_operand.node.location)
|
73
|
+
return unless loc
|
74
|
+
|
75
|
+
message = offense_message(join_operand)
|
76
|
+
add_offense(loc, message: message)
|
77
|
+
end
|
78
|
+
|
79
|
+
# @param join_operand [RuboCop::Isucon::GDA::JoinOperand]
|
80
|
+
def offense_message(join_operand)
|
81
|
+
generate_offense_message(table_name: join_operand.table_name, column_name: join_operand.column_name)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,39 @@
|
|
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::ManyJoinTable}
|
8
|
+
# and {RuboCop::Cop::Isucon::Sqlite3::ManyJoinTable}
|
9
|
+
module ManyJoinTableMethods
|
10
|
+
MSG = "Avoid SQL with lots of JOINs"
|
11
|
+
|
12
|
+
# @param node [RuboCop::AST::Node]
|
13
|
+
def on_send(node)
|
14
|
+
with_db_query(node) do |_, root_gda|
|
15
|
+
check_and_register_offence(root_gda: root_gda, node: node)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# @param root_gda [RuboCop::Isucon::GDA::Client]
|
22
|
+
# @param node [RuboCop::AST::Node]
|
23
|
+
def check_and_register_offence(root_gda:, node:)
|
24
|
+
return unless root_gda
|
25
|
+
|
26
|
+
root_gda.visit_all do |gda|
|
27
|
+
add_offense(node) if gda.table_names.count > count_tables
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Integer]
|
32
|
+
def count_tables
|
33
|
+
cop_config["CountTables"]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -8,6 +8,8 @@ module RuboCop
|
|
8
8
|
module Mysql2XqueryMethods
|
9
9
|
extend NodePattern::Macros
|
10
10
|
|
11
|
+
include OffenceLocationMethods
|
12
|
+
|
11
13
|
# @!method find_xquery(node)
|
12
14
|
# @param node [RuboCop::AST::Node]
|
13
15
|
def_node_search :find_xquery, <<~PATTERN
|
@@ -22,7 +24,7 @@ module RuboCop
|
|
22
24
|
# @yieldparam root_gda [RuboCop::Isucon::GDA::Client,nil]
|
23
25
|
#
|
24
26
|
# @note If arguments of `db.xquery` isn't string, `root_gda` is `nil`
|
25
|
-
def
|
27
|
+
def with_db_query(node)
|
26
28
|
find_xquery(node) do |type, params|
|
27
29
|
sql = xquery_param(type: type, params: params)
|
28
30
|
|
@@ -36,22 +38,13 @@ module RuboCop
|
|
36
38
|
end
|
37
39
|
end
|
38
40
|
|
39
|
-
|
40
|
-
# @param node [RuboCop::AST::Node]
|
41
|
-
# @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
|
42
|
-
# @return [Parser::Source::Range,nil]
|
43
|
-
def offense_location(type:, node:, gda_location:)
|
44
|
-
return nil unless gda_location
|
45
|
-
|
46
|
-
begin_pos = begin_position_from_gda_location(type: type, node: node, gda_location: gda_location)
|
47
|
-
return nil unless begin_pos
|
41
|
+
private
|
48
42
|
|
49
|
-
|
50
|
-
|
43
|
+
# @return [Array<Symbol>]
|
44
|
+
def db_query_methods
|
45
|
+
%i[xquery query]
|
51
46
|
end
|
52
47
|
|
53
|
-
private
|
54
|
-
|
55
48
|
# @param type [Symbol] Node type. one of `:str`, `:dstr`
|
56
49
|
# @param params [Array<RuboCop::AST::Node>]
|
57
50
|
# @return [String,nil]
|
@@ -67,108 +60,6 @@ module RuboCop
|
|
67
60
|
end
|
68
61
|
nil
|
69
62
|
end
|
70
|
-
|
71
|
-
# @param type [Symbol] Node type. one of `:str`, `:dstr`
|
72
|
-
# @param node [RuboCop::AST::Node]
|
73
|
-
# @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
|
74
|
-
# @return [Integer,nil]
|
75
|
-
def begin_position_from_gda_location(type:, node:, gda_location:)
|
76
|
-
case type
|
77
|
-
when :str
|
78
|
-
return begin_position_from_gda_location_for_str(node: node, gda_location: gda_location)
|
79
|
-
when :dstr
|
80
|
-
return begin_position_from_gda_location_for_dstr(node: node, gda_location: gda_location)
|
81
|
-
end
|
82
|
-
|
83
|
-
nil
|
84
|
-
end
|
85
|
-
|
86
|
-
# @param node [RuboCop::AST::Node]
|
87
|
-
# @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
|
88
|
-
# @return [Integer,nil]
|
89
|
-
def begin_position_from_gda_location_for_str(node:, gda_location:)
|
90
|
-
str_node = node.child_nodes[1]
|
91
|
-
return nil unless str_node&.str_type?
|
92
|
-
|
93
|
-
str_node.loc.begin.end_pos + gda_location.begin_pos
|
94
|
-
end
|
95
|
-
|
96
|
-
# @param node [RuboCop::AST::Node]
|
97
|
-
# @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
|
98
|
-
# @return [Integer,nil]
|
99
|
-
def begin_position_from_gda_location_for_dstr(node:, gda_location:)
|
100
|
-
dstr_node = node.child_nodes[1]
|
101
|
-
return nil unless dstr_node&.dstr_type?
|
102
|
-
|
103
|
-
str_node = find_str_node_from_gda_location(dstr_node: dstr_node, gda_location: gda_location)
|
104
|
-
index = str_node.value.index(gda_location.body)
|
105
|
-
return nil unless index
|
106
|
-
|
107
|
-
str_node_begin_pos(str_node) + index + heredoc_indent_level(node)
|
108
|
-
end
|
109
|
-
|
110
|
-
# @param str_node [RuboCop::AST::StrNode]
|
111
|
-
# @return [Integer]
|
112
|
-
def str_node_begin_pos(str_node)
|
113
|
-
begin_pos = str_node.loc.expression.begin_pos
|
114
|
-
|
115
|
-
# e.g.
|
116
|
-
# db.xquery(
|
117
|
-
# "SELECT * " \
|
118
|
-
# "FROM users " \
|
119
|
-
# "LIMIT 10"
|
120
|
-
# )
|
121
|
-
return begin_pos + 1 if str_node.loc.expression.source_buffer.source[begin_pos] == '"'
|
122
|
-
|
123
|
-
begin_pos
|
124
|
-
end
|
125
|
-
|
126
|
-
# @param dstr_node [RuboCop::AST::DstrNode]
|
127
|
-
# @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
|
128
|
-
# @return [RuboCop::AST::StrNode,nil]
|
129
|
-
def find_str_node_from_gda_location(dstr_node:, gda_location:)
|
130
|
-
return nil unless dstr_node
|
131
|
-
|
132
|
-
begin_pos = 0
|
133
|
-
dstr_node.child_nodes.each do |str_node|
|
134
|
-
return str_node if begin_pos <= gda_location.begin_pos && gda_location.begin_pos < begin_pos + str_node.value.length
|
135
|
-
|
136
|
-
begin_pos += str_node.value.length
|
137
|
-
end
|
138
|
-
nil
|
139
|
-
end
|
140
|
-
|
141
|
-
# @param node [RuboCop::AST::Node]
|
142
|
-
# @return [Integer]
|
143
|
-
def heredoc_indent_level(node)
|
144
|
-
dstr_node = node.child_nodes[1]
|
145
|
-
return 0 unless dstr_node&.dstr_type?
|
146
|
-
|
147
|
-
heredoc_indent_type = heredoc_indent_type(node)
|
148
|
-
return 0 unless heredoc_indent_type == "~"
|
149
|
-
|
150
|
-
heredoc_body = dstr_node.loc.heredoc_body.source
|
151
|
-
indent_level(heredoc_body)
|
152
|
-
end
|
153
|
-
|
154
|
-
# @param str [String]
|
155
|
-
# @return [Integer]
|
156
|
-
# @see https://github.com/rubocop/rubocop/blob/v1.21.0/lib/rubocop/cop/mixin/heredoc.rb#L23-L28
|
157
|
-
def indent_level(str)
|
158
|
-
indentations = str.lines.
|
159
|
-
map { |line| line[/^\s*/] }.
|
160
|
-
reject { |line| line.end_with?("\n") }
|
161
|
-
indentations.empty? ? 0 : indentations.min_by(&:size).size
|
162
|
-
end
|
163
|
-
|
164
|
-
# Returns '~', '-' or nil
|
165
|
-
#
|
166
|
-
# @param node [RuboCop::AST::Node]
|
167
|
-
# @return [String,nil] '~', '-' or `nil`
|
168
|
-
# @see https://github.com/rubocop/rubocop/blob/v1.21.0/lib/rubocop/cop/layout/heredoc_indentation.rb#L146-L149
|
169
|
-
def heredoc_indent_type(node)
|
170
|
-
node.source[/<<([~-])/, 1]
|
171
|
-
end
|
172
63
|
end
|
173
64
|
end
|
174
65
|
end
|
@@ -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
|