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.
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