rubocop-isucon 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +10 -0
  3. data/.github/workflows/pages.yml +68 -0
  4. data/.github/workflows/test.yml +3 -7
  5. data/.rubocop.yml +5 -1
  6. data/CHANGELOG.md +21 -1
  7. data/README.md +24 -6
  8. data/config/default.yml +37 -1
  9. data/gemfiles/activerecord_7_1.gemfile +14 -0
  10. data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector → n_plus_one_query_corrector}/correctable_methods.rb +1 -1
  11. data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector → n_plus_one_query_corrector}/replace_methods.rb +1 -1
  12. data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector.rb → n_plus_one_query_corrector.rb} +16 -5
  13. data/lib/rubocop/cop/isucon/mixin/join_without_index_methods.rb +87 -0
  14. data/lib/rubocop/cop/isucon/mixin/many_join_table_methods.rb +39 -0
  15. data/lib/rubocop/cop/isucon/mixin/mysql2_xquery_methods.rb +7 -116
  16. data/lib/rubocop/cop/isucon/mixin/n_plus_one_query_methods.rb +153 -0
  17. data/lib/rubocop/cop/isucon/mixin/offense_location_methods.rb +130 -0
  18. data/lib/rubocop/cop/isucon/mixin/select_asterisk_methods.rb +148 -0
  19. data/lib/rubocop/cop/isucon/mixin/sqlite3_execute_methods.rb +67 -0
  20. data/lib/rubocop/cop/isucon/mixin/where_without_index_methods.rb +96 -0
  21. data/lib/rubocop/cop/isucon/mysql2/join_without_index.rb +4 -67
  22. data/lib/rubocop/cop/isucon/mysql2/many_join_table.rb +1 -26
  23. data/lib/rubocop/cop/isucon/mysql2/n_plus_one_query.rb +5 -115
  24. data/lib/rubocop/cop/isucon/mysql2/select_asterisk.rb +1 -135
  25. data/lib/rubocop/cop/isucon/mysql2/where_without_index.rb +7 -72
  26. data/lib/rubocop/cop/isucon/shell/backtick.rb +7 -0
  27. data/lib/rubocop/cop/isucon/sqlite3/join_without_index.rb +37 -0
  28. data/lib/rubocop/cop/isucon/sqlite3/many_join_table.rb +61 -0
  29. data/lib/rubocop/cop/isucon/sqlite3/n_plus_one_query.rb +70 -0
  30. data/lib/rubocop/cop/isucon/sqlite3/select_asterisk.rb +37 -0
  31. data/lib/rubocop/cop/isucon/sqlite3/where_without_index.rb +40 -0
  32. data/lib/rubocop/cop/isucon_cops.rb +13 -1
  33. data/lib/rubocop/isucon/gda/client.rb +1 -1
  34. data/lib/rubocop/isucon/gda/join_operand.rb +1 -1
  35. data/lib/rubocop/isucon/version.rb +1 -1
  36. data/rubocop-isucon.gemspec +3 -2
  37. metadata +38 -10
  38. data/.github/workflows/gh-pages.yml +0 -44
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c235faec7d2edaadc48681992ad139ffc81d0745e010d923132a509f8012daa3
4
- data.tar.gz: eecb16448d52f4a48860171045e9fc6b6108ce68db7d5aa4601a6fcc865828b0
3
+ metadata.gz: 6bd0eac249a23368414a86aa1ab08826542cf644fa50b19017a2bcfd6648d594
4
+ data.tar.gz: 18e829ac97ac7c7cb9696a1078f36b8a958fd340ddbb9bc1b232b2630e683df2
5
5
  SHA512:
6
- metadata.gz: 2a82eca957548836926ef797d18ed5e014ab26c03407b1bdfd82a659b082479f1d5ede81c05baacd0c172b5856b38b839262fb9ba238618b03597f524f2ddeda
7
- data.tar.gz: 3571157d23e59636c0af87617111d0146c00dedfe3da74f4b64fd4e4e108886ef32d1fa56f65f531578be251eca6f0c8182a263cec18c098c497cd9fcc8be6a3
6
+ metadata.gz: 58238a855da0596f653d47436977b44cc7722982a1b59e60850f79003b356d85071cba061cca6adae2b4cfa4b63ab489e121135e3c2954f366f5ca1c375e1bbf
7
+ data.tar.gz: b7747989e1219c7fee4f834b8811e419d3c9f6083336d5fab7e7b19a3f8e1f0dad86644e708b18b926b9de3a678edf8b31495097e319bff970599b80d0ab21d9
@@ -0,0 +1,10 @@
1
+ # c.f. https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
2
+ version: 2
3
+
4
+ updates:
5
+ - package-ecosystem: github-actions
6
+ directory: /
7
+ schedule:
8
+ interval: weekly
9
+ assignees:
10
+ - sue445
@@ -0,0 +1,68 @@
1
+ # Simple workflow for deploying static content to GitHub Pages
2
+ name: Deploy static content to Pages
3
+
4
+ on:
5
+ # Runs on pushes targeting the default branch
6
+ push:
7
+ branches:
8
+ - main
9
+
10
+ # Allows you to run this workflow manually from the Actions tab
11
+ workflow_dispatch:
12
+
13
+ # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
14
+ permissions:
15
+ contents: read
16
+ pages: write
17
+ id-token: write
18
+
19
+ # Allow one concurrent deployment
20
+ concurrency:
21
+ group: "pages"
22
+ cancel-in-progress: true
23
+
24
+ jobs:
25
+ # Single deploy job since we're just deploying
26
+ deploy:
27
+ environment:
28
+ name: github-pages
29
+ url: ${{ steps.deployment.outputs.page_url }}
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - name: Checkout
33
+ uses: actions/checkout@v4
34
+
35
+ - name: Install packages
36
+ run: |
37
+ set -xe
38
+ sudo apt-get update
39
+ sudo apt-get install -y libgda-5.0
40
+
41
+ - uses: ruby/setup-ruby@v1
42
+ with:
43
+ ruby-version: ruby
44
+ bundler-cache: true
45
+
46
+ - run: bundle exec yard
47
+
48
+ - name: Setup Pages
49
+ uses: actions/configure-pages@v3
50
+ - name: Upload artifact
51
+ uses: actions/upload-pages-artifact@v2
52
+ with:
53
+ # Upload entire repository
54
+ path: './doc'
55
+ - name: Deploy to GitHub Pages
56
+ id: deployment
57
+ uses: actions/deploy-pages@main
58
+
59
+ - name: Slack Notification (not success)
60
+ uses: lazy-actions/slatify@master
61
+ if: "! success()"
62
+ continue-on-error: true
63
+ with:
64
+ job_name: "*pages*"
65
+ type: ${{ job.status }}
66
+ icon_emoji: ":octocat:"
67
+ url: ${{ secrets.SLACK_WEBHOOK }}
68
+ token: ${{ secrets.GITHUB_TOKEN }}
@@ -21,23 +21,19 @@ jobs:
21
21
 
22
22
  matrix:
23
23
  ruby:
24
- - "2.6"
25
- - "2.7"
26
24
  - "3.0"
27
25
  - "3.1"
26
+ - "3.2"
28
27
  gemfile:
29
28
  - activerecord_6_1
30
29
  - activerecord_7_0
31
- exclude:
32
- # activerecord 7.0+ requires Ruby 2.7+
33
- - ruby: "2.6"
34
- gemfile: activerecord_7_0
30
+ - activerecord_7_1
35
31
 
36
32
  env:
37
33
  BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
38
34
 
39
35
  steps:
40
- - uses: actions/checkout@v2
36
+ - uses: actions/checkout@v4
41
37
 
42
38
  - name: Install packages
43
39
  run: |
data/.rubocop.yml CHANGED
@@ -1,10 +1,11 @@
1
1
  require:
2
2
  - rubocop-performance
3
+ - rubocop-yard
3
4
 
4
5
  AllCops:
5
6
  NewCops: enable
6
7
  SuggestExtensions: false
7
- TargetRubyVersion: 2.6
8
+ TargetRubyVersion: 3.0
8
9
  Exclude:
9
10
  - 'gemfiles/vendor/**/*'
10
11
  - 'benchmark/**/*'
@@ -15,6 +16,9 @@ AllCops:
15
16
  - 'vendor/**/*'
16
17
  - '.git/**/*'
17
18
 
19
+ Gemspec/DevelopmentDependencies:
20
+ EnforcedStyle: gemspec
21
+
18
22
  Layout/DotPosition:
19
23
  EnforcedStyle: trailing
20
24
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
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/v1.0.0...main)
3
+
4
+ ## [1.0.0] - 2023-10-29
5
+ [full changelog](http://github.com/sue445/rubocop-isucon/compare/v0.2.0...v1.0.0)
6
+
7
+ * Requires Ruby 3.0+
8
+ * https://github.com/sue445/rubocop-isucon/pull/236
9
+
10
+ ## [0.2.0] - 2022-07-31
11
+ [full changelog](http://github.com/sue445/rubocop-isucon/compare/v0.1.0...v0.2.0)
12
+
13
+ * Add `Isucon/Sqlite3/SelectAsterisk`
14
+ * https://github.com/sue445/rubocop-isucon/pull/202
15
+ * Add `Isucon/Sqlite3/WhereWithoutIndex`
16
+ * https://github.com/sue445/rubocop-isucon/pull/207
17
+ * Add `Isucon/Sqlite3/JoinWithoutIndex`
18
+ * https://github.com/sue445/rubocop-isucon/pull/208
19
+ * Add `Isucon/Sqlite3/ManyJoinTable`
20
+ * https://github.com/sue445/rubocop-isucon/pull/209
21
+ * Add `Isucon/Sqlite3/NPlusOneQuery`
22
+ * https://github.com/sue445/rubocop-isucon/pull/210
3
23
 
4
24
  ## [0.1.0] - 2022-07-23
5
25
 
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/)
@@ -106,3 +117,10 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/sue445
106
117
  ## License
107
118
 
108
119
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
120
+
121
+ ISUCON is a trademark or registered trademark of LINE Corporation.
122
+
123
+ https://isucon.net
124
+
125
+ ## Presentation
126
+ * [Fix SQL N\+1 queries with RuboCop](https://speakerdeck.com/sue445/fix-sql-n-plus-one-queries-with-rubocop) at [RubyKaigi 2023](https://rubykaigi.org/2013/) :gem:
data/config/default.yml CHANGED
@@ -16,7 +16,7 @@ Isucon/Mysql2/ManyJoinTable:
16
16
  CountTables: 3
17
17
 
18
18
  Isucon/Mysql2/NPlusOneQuery:
19
- Description: 'Checks that N+1 query is not used'
19
+ Description: 'Checks that there’s no N+1 query'
20
20
  Enabled: true
21
21
  VersionAdded: '0.1.0'
22
22
  SafeAutoCorrect: false
@@ -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 there’s no N+1 query'
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
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 7.1.0"
6
+
7
+ group :sqlite3 do
8
+ # c.f. https://github.com/rails/rails/blob/v7.1.0/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L14
9
+ gem "sqlite3", "~> 1.4"
10
+ end
11
+
12
+ # eval_gemfile "#{__dir__}/common.gemfile"
13
+
14
+ gemspec path: "../"
@@ -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
@@ -9,7 +9,7 @@ module RuboCop
9
9
  module Correctors
10
10
  # rubocop:disable Layout/LineLength
11
11
 
12
- # Corrector for {RuboCop::Cop::Isucon::Mysql2::NPlusOneQuery}
12
+ # Corrector for {RuboCop::Cop::Isucon::Mysql2::NPlusOneQuery} and {RuboCop::Cop::Isucon::Sqlite3::NPlusOneQuery}
13
13
  #
14
14
  # @example Before
15
15
  # courses.map do |course|
@@ -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