rubocop-isucon 0.1.0 → 1.0.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 (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