rubocop-isucon 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -1
  3. data/README.md +17 -6
  4. data/config/default.yml +36 -0
  5. data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector → n_plus_one_query_corrector}/correctable_methods.rb +1 -1
  6. data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector → n_plus_one_query_corrector}/replace_methods.rb +1 -1
  7. data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector.rb → n_plus_one_query_corrector.rb} +15 -4
  8. data/lib/rubocop/cop/isucon/mixin/join_without_index_methods.rb +87 -0
  9. data/lib/rubocop/cop/isucon/mixin/many_join_table_methods.rb +39 -0
  10. data/lib/rubocop/cop/isucon/mixin/mysql2_xquery_methods.rb +7 -116
  11. data/lib/rubocop/cop/isucon/mixin/n_plus_one_query_methods.rb +153 -0
  12. data/lib/rubocop/cop/isucon/mixin/offense_location_methods.rb +130 -0
  13. data/lib/rubocop/cop/isucon/mixin/select_asterisk_methods.rb +148 -0
  14. data/lib/rubocop/cop/isucon/mixin/sqlite3_execute_methods.rb +67 -0
  15. data/lib/rubocop/cop/isucon/mixin/where_without_index_methods.rb +96 -0
  16. data/lib/rubocop/cop/isucon/mysql2/join_without_index.rb +4 -67
  17. data/lib/rubocop/cop/isucon/mysql2/many_join_table.rb +1 -26
  18. data/lib/rubocop/cop/isucon/mysql2/n_plus_one_query.rb +4 -114
  19. data/lib/rubocop/cop/isucon/mysql2/select_asterisk.rb +1 -135
  20. data/lib/rubocop/cop/isucon/mysql2/where_without_index.rb +5 -70
  21. data/lib/rubocop/cop/isucon/sqlite3/join_without_index.rb +37 -0
  22. data/lib/rubocop/cop/isucon/sqlite3/many_join_table.rb +61 -0
  23. data/lib/rubocop/cop/isucon/sqlite3/n_plus_one_query.rb +70 -0
  24. data/lib/rubocop/cop/isucon/sqlite3/select_asterisk.rb +37 -0
  25. data/lib/rubocop/cop/isucon/sqlite3/where_without_index.rb +40 -0
  26. data/lib/rubocop/cop/isucon_cops.rb +13 -1
  27. data/lib/rubocop/isucon/version.rb +1 -1
  28. metadata +17 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c235faec7d2edaadc48681992ad139ffc81d0745e010d923132a509f8012daa3
4
- data.tar.gz: eecb16448d52f4a48860171045e9fc6b6108ce68db7d5aa4601a6fcc865828b0
3
+ metadata.gz: 645e1a353538a4c8d8c8914ac76ac7fbb6f1338dbea8d10c16394a6711be725a
4
+ data.tar.gz: 450b780a05d2e4f5632bd2ffe35453390bfa1e1c4a240662e440b5cbb2e5390c
5
5
  SHA512:
6
- metadata.gz: 2a82eca957548836926ef797d18ed5e014ab26c03407b1bdfd82a659b082479f1d5ede81c05baacd0c172b5856b38b839262fb9ba238618b03597f524f2ddeda
7
- data.tar.gz: 3571157d23e59636c0af87617111d0146c00dedfe3da74f4b64fd4e4e108886ef32d1fa56f65f531578be251eca6f0c8182a263cec18c098c497cd9fcc8be6a3
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.1.0...main)
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
+ [![Gem Version](https://badge.fury.io/rb/rubocop-isucon.svg)](https://badge.fury.io/rb/rubocop-isucon)
4
5
  [![Build Status](https://github.com/sue445/rubocop-isucon/workflows/test/badge.svg?branch=main)](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 | offense detection | auto-correct |
80
- |-----------------------------------|----------------------------|----------------------------|
81
- | `Isucon/Mysql2/JoinWithoutIndex` | `Database` is **required** | Not supported |
82
- | `Isucon/Mysql2/NPlusOneQuery` | `Database` is optional | `Database` is **required** |
83
- | `Isucon/Mysql2/SelectAsterisk` | `Database` is optional | `Database` is **required** |
84
- | `Isucon/Mysql2/WhereWithoutIndex` | `Database` is **required** | Not supported |
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
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module Cop
5
5
  module Isucon
6
6
  module Correctors
7
- class Mysql2NPlusOneQueryCorrector
7
+ class NPlusOneQueryCorrector
8
8
  # Check whether can correct
9
9
  module CorrectableMethods
10
10
  # @return [Boolean]
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module Cop
5
5
  module Isucon
6
6
  module Correctors
7
- class Mysql2NPlusOneQueryCorrector
7
+ class NPlusOneQueryCorrector
8
8
  # replace ast
9
9
  module ReplaceMethods
10
10
  def replace
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "mysql2_n_plus_one_query_corrector/correctable_methods"
4
- require_relative "mysql2_n_plus_one_query_corrector/replace_methods"
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 Mysql2NPlusOneQueryCorrector
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
- def initialize(corrector:, current_node:, parent_node:, type:, gda:, connection:) # rubocop:disable Metrics/ParameterLists
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 with_xquery(node)
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
- # @param type [Symbol] Node type. one of `:str`, `:dstr`
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
- end_pos = begin_pos + gda_location.length
50
- Parser::Source::Range.new(node.loc.expression.source_buffer, begin_pos, end_pos)
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