rubocop-isucon 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.github/workflows/gh-pages.yml +44 -0
- data/.github/workflows/test.yml +91 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +43 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +108 -0
- data/Rakefile +35 -0
- data/benchmark/README.md +69 -0
- data/benchmark/memorize.rb +86 -0
- data/benchmark/parse_table.rb +103 -0
- data/benchmark/shell.rb +26 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/config/default.yml +83 -0
- data/config/enable-only-performance.yml +30 -0
- data/gemfiles/activerecord_6_1.gemfile +14 -0
- data/gemfiles/activerecord_7_0.gemfile +14 -0
- data/lib/rubocop/cop/isucon/correctors/mysql2_n_plus_one_query_corrector/correctable_methods.rb +66 -0
- data/lib/rubocop/cop/isucon/correctors/mysql2_n_plus_one_query_corrector/replace_methods.rb +127 -0
- data/lib/rubocop/cop/isucon/correctors/mysql2_n_plus_one_query_corrector.rb +112 -0
- data/lib/rubocop/cop/isucon/mixin/database_methods.rb +59 -0
- data/lib/rubocop/cop/isucon/mixin/mysql2_xquery_methods.rb +176 -0
- data/lib/rubocop/cop/isucon/mixin/sinatra_methods.rb +37 -0
- data/lib/rubocop/cop/isucon/mysql2/join_without_index.rb +100 -0
- data/lib/rubocop/cop/isucon/mysql2/many_join_table.rb +86 -0
- data/lib/rubocop/cop/isucon/mysql2/n_plus_one_query.rb +179 -0
- data/lib/rubocop/cop/isucon/mysql2/prepare_execute.rb +136 -0
- data/lib/rubocop/cop/isucon/mysql2/select_asterisk.rb +171 -0
- data/lib/rubocop/cop/isucon/mysql2/where_without_index.rb +105 -0
- data/lib/rubocop/cop/isucon/shell/backtick.rb +36 -0
- data/lib/rubocop/cop/isucon/shell/system.rb +36 -0
- data/lib/rubocop/cop/isucon/sinatra/disable_logging.rb +83 -0
- data/lib/rubocop/cop/isucon/sinatra/logger.rb +52 -0
- data/lib/rubocop/cop/isucon/sinatra/rack_logger.rb +58 -0
- data/lib/rubocop/cop/isucon/sinatra/serve_static_file.rb +73 -0
- data/lib/rubocop/cop/isucon_cops.rb +20 -0
- data/lib/rubocop/isucon/database_connection.rb +42 -0
- data/lib/rubocop/isucon/gda/client.rb +184 -0
- data/lib/rubocop/isucon/gda/gda_ext.rb +119 -0
- data/lib/rubocop/isucon/gda/join_condition.rb +25 -0
- data/lib/rubocop/isucon/gda/join_operand.rb +46 -0
- data/lib/rubocop/isucon/gda/node_location.rb +42 -0
- data/lib/rubocop/isucon/gda/node_patcher.rb +101 -0
- data/lib/rubocop/isucon/gda/where_condition.rb +73 -0
- data/lib/rubocop/isucon/gda/where_operand.rb +32 -0
- data/lib/rubocop/isucon/gda.rb +28 -0
- data/lib/rubocop/isucon/inject.rb +20 -0
- data/lib/rubocop/isucon/memorize_methods.rb +38 -0
- data/lib/rubocop/isucon/version.rb +7 -0
- data/lib/rubocop/isucon.rb +20 -0
- data/lib/rubocop-isucon.rb +16 -0
- data/rubocop-isucon.gemspec +52 -0
- metadata +286 -0
| @@ -0,0 +1,176 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module RuboCop
         | 
| 4 | 
            +
              module Cop
         | 
| 5 | 
            +
                module Isucon
         | 
| 6 | 
            +
                  module Mixin
         | 
| 7 | 
            +
                    # Helper methods for `db.xquery` or `db.query` in AST
         | 
| 8 | 
            +
                    module Mysql2XqueryMethods
         | 
| 9 | 
            +
                      extend NodePattern::Macros
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                      # @!method find_xquery(node)
         | 
| 12 | 
            +
                      #   @param node [RuboCop::AST::Node]
         | 
| 13 | 
            +
                      def_node_search :find_xquery, <<~PATTERN
         | 
| 14 | 
            +
                        (send (send nil? _) {:xquery | :query} (${str dstr lvar ivar cvar} $...) ...)
         | 
| 15 | 
            +
                      PATTERN
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                      NON_STRING_WARNING_MSG = "Warning: non-string was passed to `query` or `xquery` 1st argument. " \
         | 
| 18 | 
            +
                                               "So argument doesn't parsed as SQL (%<file_path>s:%<line_num>d)"
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                      # @param node [RuboCop::AST::Node]
         | 
| 21 | 
            +
                      # @yieldparam type [Symbol] Node type. one of `:str`, `:dstr`
         | 
| 22 | 
            +
                      # @yieldparam root_gda [RuboCop::Isucon::GDA::Client,nil]
         | 
| 23 | 
            +
                      #
         | 
| 24 | 
            +
                      # @note If arguments of `db.xquery` isn't string, `root_gda` is `nil`
         | 
| 25 | 
            +
                      def with_xquery(node)
         | 
| 26 | 
            +
                        find_xquery(node) do |type, params|
         | 
| 27 | 
            +
                          sql = xquery_param(type: type, params: params)
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                          unless sql
         | 
| 30 | 
            +
                            warn format(NON_STRING_WARNING_MSG, file_path: processed_source.file_path, line_num: node.loc.expression.line)
         | 
| 31 | 
            +
                          end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                          root_gda = sql ? RuboCop::Isucon::GDA::Client.new(sql) : nil
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                          yield type, root_gda
         | 
| 36 | 
            +
                        end
         | 
| 37 | 
            +
                      end
         | 
| 38 | 
            +
             | 
| 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
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                        end_pos = begin_pos + gda_location.length
         | 
| 50 | 
            +
                        Parser::Source::Range.new(node.loc.expression.source_buffer, begin_pos, end_pos)
         | 
| 51 | 
            +
                      end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                      private
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                      # @param type [Symbol] Node type. one of `:str`, `:dstr`
         | 
| 56 | 
            +
                      # @param params [Array<RuboCop::AST::Node>]
         | 
| 57 | 
            +
                      # @return [String,nil]
         | 
| 58 | 
            +
                      def xquery_param(type:, params:)
         | 
| 59 | 
            +
                        case type
         | 
| 60 | 
            +
                        when :str
         | 
| 61 | 
            +
                          return params[0]
         | 
| 62 | 
            +
                        when :dstr
         | 
| 63 | 
            +
                          if params.all? { |param| param.respond_to?(:value) }
         | 
| 64 | 
            +
                            # heredoc
         | 
| 65 | 
            +
                            return params.map(&:value).join
         | 
| 66 | 
            +
                          end
         | 
| 67 | 
            +
                        end
         | 
| 68 | 
            +
                        nil
         | 
| 69 | 
            +
                      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 | 
            +
                    end
         | 
| 173 | 
            +
                  end
         | 
| 174 | 
            +
                end
         | 
| 175 | 
            +
              end
         | 
| 176 | 
            +
            end
         | 
| @@ -0,0 +1,37 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module RuboCop
         | 
| 4 | 
            +
              module Cop
         | 
| 5 | 
            +
                module Isucon
         | 
| 6 | 
            +
                  module Mixin
         | 
| 7 | 
            +
                    # Helper methods for {RuboCop::Cop::Isucon::Sinatra}
         | 
| 8 | 
            +
                    module SinatraMethods
         | 
| 9 | 
            +
                      extend NodePattern::Macros
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                      # @!method subclass_of_sinatra_base?(node)
         | 
| 12 | 
            +
                      #   Whether match to `class AnyClass < Sinatra::Base` node
         | 
| 13 | 
            +
                      #   @param node [RuboCop::AST::Node]
         | 
| 14 | 
            +
                      #   @return [Boolean]
         | 
| 15 | 
            +
                      def_node_matcher :subclass_of_sinatra_base?, <<~PATTERN
         | 
| 16 | 
            +
                        (class (const nil? _) (const (const nil? :Sinatra) :Base) ...)
         | 
| 17 | 
            +
                      PATTERN
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                      # @!method subclass_of_sinatra_base_contains_logging?(node)
         | 
| 20 | 
            +
                      #   Whether match to `class AnyClass < Sinatra::Base` node and contains :logging configuration
         | 
| 21 | 
            +
                      #   @param node [RuboCop::AST::Node]
         | 
| 22 | 
            +
                      #   @return [Boolean]
         | 
| 23 | 
            +
                      def_node_matcher :subclass_of_sinatra_base_contains_logging?, <<~PATTERN
         | 
| 24 | 
            +
                        (class (const nil? _) (const (const nil? :Sinatra) :Base) ... `(send nil? _ (sym :logging)))
         | 
| 25 | 
            +
                      PATTERN
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                      # Whether parent node match to `class AnyClass < Sinatra::Base` node
         | 
| 28 | 
            +
                      # @param node [RuboCop::AST::Node]
         | 
| 29 | 
            +
                      # @return [Boolean]
         | 
| 30 | 
            +
                      def parent_is_sinatra_app?(node)
         | 
| 31 | 
            +
                        node.each_ancestor.any? { |ancestor| subclass_of_sinatra_base?(ancestor) }
         | 
| 32 | 
            +
                      end
         | 
| 33 | 
            +
                    end
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
            end
         | 
| @@ -0,0 +1,100 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module RuboCop
         | 
| 4 | 
            +
              module Cop
         | 
| 5 | 
            +
                module Isucon
         | 
| 6 | 
            +
                  module Mysql2
         | 
| 7 | 
            +
                    # Check for `JOIN` without index
         | 
| 8 | 
            +
                    #
         | 
| 9 | 
            +
                    # @note If `Database` isn't configured, this cop's feature (offense detection and auto-correct) will not be available.
         | 
| 10 | 
            +
                    #
         | 
| 11 | 
            +
                    # @example
         | 
| 12 | 
            +
                    #   # bad (user_id is not indexed)
         | 
| 13 | 
            +
                    #   db.xquery('SELECT id, title FROM articles JOIN users ON users.id = articles.user_id')
         | 
| 14 | 
            +
                    #
         | 
| 15 | 
            +
                    #   # good (user_id is indexed)
         | 
| 16 | 
            +
                    #   db.xquery('SELECT id, title FROM articles JOIN users ON users.id = articles.user_id')
         | 
| 17 | 
            +
                    #
         | 
| 18 | 
            +
                    class JoinWithoutIndex < Base
         | 
| 19 | 
            +
                      include Mixin::DatabaseMethods
         | 
| 20 | 
            +
                      include Mixin::Mysql2XqueryMethods
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                      MSG = "This join clause doesn't seem to have an index. " \
         | 
| 23 | 
            +
                            "(e.g. `ALTER TABLE %<table_name>s ADD INDEX index_%<column_name>s (%<column_name>s)`)"
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                      # @param node [RuboCop::AST::Node]
         | 
| 26 | 
            +
                      def on_send(node)
         | 
| 27 | 
            +
                        with_error_handling(node) do
         | 
| 28 | 
            +
                          return unless enabled_database?
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                          with_xquery(node) do |type, root_gda|
         | 
| 31 | 
            +
                            check_and_register_offence(type: type, root_gda: root_gda, node: node)
         | 
| 32 | 
            +
                          end
         | 
| 33 | 
            +
                        end
         | 
| 34 | 
            +
                      end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                      private
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                      # @param type [Symbol] Node type. one of `:str`, `:dstr`
         | 
| 39 | 
            +
                      # @param root_gda [RuboCop::Isucon::GDA::Client]
         | 
| 40 | 
            +
                      # @param node [RuboCop::AST::Node]
         | 
| 41 | 
            +
                      def check_and_register_offence(type:, root_gda:, node:)
         | 
| 42 | 
            +
                        return unless root_gda
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                        root_gda.visit_all do |gda|
         | 
| 45 | 
            +
                          gda.join_conditions.each do |join_condition|
         | 
| 46 | 
            +
                            join_operand = join_operand_without_index(join_condition)
         | 
| 47 | 
            +
                            next unless join_operand
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                            register_offense(type: type, node: node, join_operand: join_operand)
         | 
| 50 | 
            +
                          end
         | 
| 51 | 
            +
                        end
         | 
| 52 | 
            +
                      end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                      # @param join_condition [RuboCop::Isucon::GDA::JoinCondition]
         | 
| 55 | 
            +
                      # @return [RuboCop::Isucon::GDA::JoinOperand,nil]
         | 
| 56 | 
            +
                      def join_operand_without_index(join_condition)
         | 
| 57 | 
            +
                        join_condition.operands.each do |join_operand|
         | 
| 58 | 
            +
                          next unless join_operand.table_name
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                          unless indexed_column?(table_name: join_operand.table_name, column_name: join_operand.column_name)
         | 
| 61 | 
            +
                            return join_operand
         | 
| 62 | 
            +
                          end
         | 
| 63 | 
            +
                        end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                        nil
         | 
| 66 | 
            +
                      end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                      # @param table_name [String]
         | 
| 69 | 
            +
                      # @param column_name [String]
         | 
| 70 | 
            +
                      # @return [Boolean]
         | 
| 71 | 
            +
                      def indexed_column?(table_name:, column_name:)
         | 
| 72 | 
            +
                        primary_keys = connection.primary_keys(table_name)
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                        return true if primary_keys&.first == column_name
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                        indexes = connection.indexes(table_name)
         | 
| 77 | 
            +
                        index_first_columns = indexes.map { |index| index.columns[0] }
         | 
| 78 | 
            +
                        index_first_columns.include?(column_name)
         | 
| 79 | 
            +
                      end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                      # @param type [Symbol] Node type. one of `:str`, `:dstr`
         | 
| 82 | 
            +
                      # @param node [RuboCop::AST::Node]
         | 
| 83 | 
            +
                      # @param join_operand [RuboCop::Isucon::GDA::JoinOperand]
         | 
| 84 | 
            +
                      def register_offense(type:, node:, join_operand:)
         | 
| 85 | 
            +
                        loc = offense_location(type: type, node: node, gda_location: join_operand.node.location)
         | 
| 86 | 
            +
                        return unless loc
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                        message = offense_message(join_operand)
         | 
| 89 | 
            +
                        add_offense(loc, message: message)
         | 
| 90 | 
            +
                      end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                      # @param join_operand [RuboCop::Isucon::GDA::JoinOperand]
         | 
| 93 | 
            +
                      def offense_message(join_operand)
         | 
| 94 | 
            +
                        format(MSG, table_name: join_operand.table_name, column_name: join_operand.column_name)
         | 
| 95 | 
            +
                      end
         | 
| 96 | 
            +
                    end
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
              end
         | 
| 100 | 
            +
            end
         | 
| @@ -0,0 +1,86 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module RuboCop
         | 
| 4 | 
            +
              module Cop
         | 
| 5 | 
            +
                module Isucon
         | 
| 6 | 
            +
                  module Mysql2
         | 
| 7 | 
            +
                    # Check if SQL contains many JOINs
         | 
| 8 | 
            +
                    #
         | 
| 9 | 
            +
                    # @example CountTables: 3 (default)
         | 
| 10 | 
            +
                    #   # bad
         | 
| 11 | 
            +
                    #   totals = db.xquery(
         | 
| 12 | 
            +
                    #     "SELECT IFNULL(SUM(`submissions`.`score`), 0) AS `total_score`" \
         | 
| 13 | 
            +
                    #     " FROM `users`" \
         | 
| 14 | 
            +
                    #     " JOIN `registrations` ON `users`.`id` = `registrations`.`user_id`" \
         | 
| 15 | 
            +
                    #     " JOIN `courses` ON `registrations`.`course_id` = `courses`.`id`" \
         | 
| 16 | 
            +
                    #     " LEFT JOIN `classes` ON `courses`.`id` = `classes`.`course_id`" \
         | 
| 17 | 
            +
                    #     " LEFT JOIN `submissions` ON `users`.`id` = `submissions`.`user_id` AND `submissions`.`class_id` = `classes`.`id`" \
         | 
| 18 | 
            +
                    #     " WHERE `courses`.`id` = ?" \
         | 
| 19 | 
            +
                    #     " GROUP BY `users`.`id`",
         | 
| 20 | 
            +
                    #     course[:id]
         | 
| 21 | 
            +
                    #   ).map { |_| _[:total_score] }
         | 
| 22 | 
            +
                    #
         | 
| 23 | 
            +
                    #   # good
         | 
| 24 | 
            +
                    #   registration_users_count =
         | 
| 25 | 
            +
                    #     db.xquery("SELECT COUNT(`user_id`) AS cnt FROM `registrations` WHERE `course_id` = ?", course[:id]).first[:cnt]
         | 
| 26 | 
            +
                    #
         | 
| 27 | 
            +
                    #   totals = db.xquery(<<~SQL, course[:id]).map { |_| _[:total_score] }
         | 
| 28 | 
            +
                    #     SELECT IFNULL(SUM(`submissions`.`score`), 0) AS `total_score`
         | 
| 29 | 
            +
                    #     FROM `submissions`
         | 
| 30 | 
            +
                    #     JOIN `classes` ON `classes`.`id` = `submissions`.`class_id`
         | 
| 31 | 
            +
                    #     WHERE `classes`.`course_id` = ?
         | 
| 32 | 
            +
                    #     GROUP BY `submissions`.`user_id`
         | 
| 33 | 
            +
                    #   SQL
         | 
| 34 | 
            +
                    #
         | 
| 35 | 
            +
                    #   if totals.count < registration_users_count
         | 
| 36 | 
            +
                    #     no_submissions_count = registration_users_count - totals.count
         | 
| 37 | 
            +
                    #     totals += [0] * no_submissions_count
         | 
| 38 | 
            +
                    #   end
         | 
| 39 | 
            +
                    #
         | 
| 40 | 
            +
                    # @example CountTables: 5
         | 
| 41 | 
            +
                    #   # good
         | 
| 42 | 
            +
                    #   totals = db.xquery(
         | 
| 43 | 
            +
                    #     "SELECT IFNULL(SUM(`submissions`.`score`), 0) AS `total_score`" \
         | 
| 44 | 
            +
                    #     " FROM `users`" \
         | 
| 45 | 
            +
                    #     " JOIN `registrations` ON `users`.`id` = `registrations`.`user_id`" \
         | 
| 46 | 
            +
                    #     " JOIN `courses` ON `registrations`.`course_id` = `courses`.`id`" \
         | 
| 47 | 
            +
                    #     " LEFT JOIN `classes` ON `courses`.`id` = `classes`.`course_id`" \
         | 
| 48 | 
            +
                    #     " LEFT JOIN `submissions` ON `users`.`id` = `submissions`.`user_id` AND `submissions`.`class_id` = `classes`.`id`" \
         | 
| 49 | 
            +
                    #     " WHERE `courses`.`id` = ?" \
         | 
| 50 | 
            +
                    #     " GROUP BY `users`.`id`",
         | 
| 51 | 
            +
                    #     course[:id]
         | 
| 52 | 
            +
                    #   ).map { |_| _[:total_score] }
         | 
| 53 | 
            +
                    #
         | 
| 54 | 
            +
                    class ManyJoinTable < Base
         | 
| 55 | 
            +
                      include Mixin::Mysql2XqueryMethods
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                      MSG = "Avoid SQL with lots of JOINs"
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                      # @param node [RuboCop::AST::Node]
         | 
| 60 | 
            +
                      def on_send(node)
         | 
| 61 | 
            +
                        with_xquery(node) do |_, root_gda|
         | 
| 62 | 
            +
                          check_and_register_offence(root_gda: root_gda, node: node)
         | 
| 63 | 
            +
                        end
         | 
| 64 | 
            +
                      end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                      private
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                      # @param root_gda [RuboCop::Isucon::GDA::Client]
         | 
| 69 | 
            +
                      # @param node [RuboCop::AST::Node]
         | 
| 70 | 
            +
                      def check_and_register_offence(root_gda:, node:)
         | 
| 71 | 
            +
                        return unless root_gda
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                        root_gda.visit_all do |gda|
         | 
| 74 | 
            +
                          add_offense(node) if gda.table_names.count > count_tables
         | 
| 75 | 
            +
                        end
         | 
| 76 | 
            +
                      end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                      # @return [Integer]
         | 
| 79 | 
            +
                      def count_tables
         | 
| 80 | 
            +
                        cop_config["CountTables"]
         | 
| 81 | 
            +
                      end
         | 
| 82 | 
            +
                    end
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
              end
         | 
| 86 | 
            +
            end
         | 
| @@ -0,0 +1,179 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module RuboCop
         | 
| 4 | 
            +
              module Cop
         | 
| 5 | 
            +
                module Isucon
         | 
| 6 | 
            +
                  module Mysql2
         | 
| 7 | 
            +
                    # rubocop:disable Layout/LineLength
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                    # Checks that N+1 query is not used
         | 
| 10 | 
            +
                    #
         | 
| 11 | 
            +
                    # @note If `Database` isn't configured, auto-correct will not be available. (Only offense detection can be used)
         | 
| 12 | 
            +
                    #
         | 
| 13 | 
            +
                    # @note For the number of N+1 queries that can be detected by this cop, there are too few that can be corrected automatically
         | 
| 14 | 
            +
                    #
         | 
| 15 | 
            +
                    # @example
         | 
| 16 | 
            +
                    #   # bad
         | 
| 17 | 
            +
                    #   reservations = db.xquery('SELECT * FROM `reservations` WHERE `schedule_id` = ?', schedule_id).map do |reservation|
         | 
| 18 | 
            +
                    #     reservation[:user] = db.xquery('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', id).first
         | 
| 19 | 
            +
                    #     reservation
         | 
| 20 | 
            +
                    #   end
         | 
| 21 | 
            +
                    #
         | 
| 22 | 
            +
                    #   # good
         | 
| 23 | 
            +
                    #   rows = db.xquery(<<~SQL, schedule_id)
         | 
| 24 | 
            +
                    #     SELECT
         | 
| 25 | 
            +
                    #       r.id AS reservation_id,
         | 
| 26 | 
            +
                    #       r.schedule_id AS reservation_schedule_id,
         | 
| 27 | 
            +
                    #       r.user_id AS reservation_user_id,
         | 
| 28 | 
            +
                    #       r.created_at AS reservation_created_at,
         | 
| 29 | 
            +
                    #       u.id AS user_id,
         | 
| 30 | 
            +
                    #       u.email AS user_email,
         | 
| 31 | 
            +
                    #       u.nickname AS user_nickname,
         | 
| 32 | 
            +
                    #       u.staff AS user_staff,
         | 
| 33 | 
            +
                    #       u.created_at AS user_created_at
         | 
| 34 | 
            +
                    #     FROM `reservations` AS r
         | 
| 35 | 
            +
                    #     INNER JOIN users u ON u.id = r.user_id
         | 
| 36 | 
            +
                    #     WHERE r.schedule_id = ?
         | 
| 37 | 
            +
                    #   SQL
         | 
| 38 | 
            +
                    #
         | 
| 39 | 
            +
                    #   # bad
         | 
| 40 | 
            +
                    #   courses.map do |course|
         | 
| 41 | 
            +
                    #     teacher = db.xquery('SELECT * FROM `users` WHERE `id` = ?', course[:teacher_id]).first
         | 
| 42 | 
            +
                    #   end
         | 
| 43 | 
            +
                    #
         | 
| 44 | 
            +
                    #   # good
         | 
| 45 | 
            +
                    #   # This is similar to ActiveRecord's preload
         | 
| 46 | 
            +
                    #   # c.f. https://guides.rubyonrails.org/active_record_querying.html#preload
         | 
| 47 | 
            +
                    #   courses.map do |course|
         | 
| 48 | 
            +
                    #     @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 }
         | 
| 49 | 
            +
                    #     teacher = @users_by_id[course[:teacher_id]]
         | 
| 50 | 
            +
                    #   end
         | 
| 51 | 
            +
                    class NPlusOneQuery < Base
         | 
| 52 | 
            +
                      # rubocop:enable Layout/LineLength
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                      include Mixin::DatabaseMethods
         | 
| 55 | 
            +
                      include Mixin::Mysql2XqueryMethods
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                      extend AutoCorrector
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                      MSG = "This looks like N+1 query."
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                      # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L38
         | 
| 62 | 
            +
                      POST_CONDITION_LOOP_TYPES = %i[while_post until_post].freeze
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                      # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L39
         | 
| 65 | 
            +
                      LOOP_TYPES = (POST_CONDITION_LOOP_TYPES + %i[while until for]).freeze
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                      # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L41
         | 
| 68 | 
            +
                      ENUMERABLE_METHOD_NAMES = (Enumerable.instance_methods + [:each]).to_set.freeze
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                      def_node_matcher :csv_loop?, <<~PATTERN
         | 
| 71 | 
            +
                        (block
         | 
| 72 | 
            +
                          (send (const nil? :CSV) :parse ...)
         | 
| 73 | 
            +
                          ...)
         | 
| 74 | 
            +
                      PATTERN
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                      # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L68
         | 
| 77 | 
            +
                      def_node_matcher :kernel_loop?, <<~PATTERN
         | 
| 78 | 
            +
                        (block
         | 
| 79 | 
            +
                          (send {nil? (const nil? :Kernel)} :loop)
         | 
| 80 | 
            +
                          ...)
         | 
| 81 | 
            +
                      PATTERN
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                      # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L74
         | 
| 84 | 
            +
                      def_node_matcher :enumerable_loop?, <<~PATTERN
         | 
| 85 | 
            +
                        (block
         | 
| 86 | 
            +
                          (send $_ #enumerable_method? ...)
         | 
| 87 | 
            +
                          ...)
         | 
| 88 | 
            +
                      PATTERN
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                      # @param node [RuboCop::AST::Node]
         | 
| 91 | 
            +
                      def on_send(node) # rubocop:disable Metrics/MethodLength
         | 
| 92 | 
            +
                        with_error_handling(node) do
         | 
| 93 | 
            +
                          with_xquery(node) do |type, root_gda|
         | 
| 94 | 
            +
                            receiver, = *node.children
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                            next unless receiver.send_type?
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                            parent = parent_loop_node(receiver)
         | 
| 99 | 
            +
                            next unless parent
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                            next if or_assignment_to_instance_variable?(node)
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                            add_offense(receiver) do |corrector|
         | 
| 104 | 
            +
                              perform_autocorrect(
         | 
| 105 | 
            +
                                corrector: corrector, current_node: receiver,
         | 
| 106 | 
            +
                                parent_node: parent, type: type, gda: root_gda
         | 
| 107 | 
            +
                              )
         | 
| 108 | 
            +
                            end
         | 
| 109 | 
            +
                          end
         | 
| 110 | 
            +
                        end
         | 
| 111 | 
            +
                      end
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                      private
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                      # Whether match to `@instance_var ||=`
         | 
| 116 | 
            +
                      # @param node [RuboCop::AST::Node]
         | 
| 117 | 
            +
                      # @return [Boolean]
         | 
| 118 | 
            +
                      def or_assignment_to_instance_variable?(node)
         | 
| 119 | 
            +
                        _or_assignment_to_instance_variable?(node.parent&.parent) ||
         | 
| 120 | 
            +
                          _or_assignment_to_instance_variable?(node.parent&.parent&.parent)
         | 
| 121 | 
            +
                      end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                      # Whether match to `@instance_var ||=`
         | 
| 124 | 
            +
                      # @param node [RuboCop::AST::Node]
         | 
| 125 | 
            +
                      # @return [Boolean]
         | 
| 126 | 
            +
                      def _or_assignment_to_instance_variable?(node)
         | 
| 127 | 
            +
                        node&.or_asgn_type? && node.child_nodes&.first&.ivasgn_type?
         | 
| 128 | 
            +
                      end
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                      # @param node [RuboCop::AST::Node]
         | 
| 131 | 
            +
                      # @return [RuboCop::AST::Node]
         | 
| 132 | 
            +
                      def parent_loop_node(node)
         | 
| 133 | 
            +
                        node.each_ancestor.find { |ancestor| loop?(ancestor, node) }
         | 
| 134 | 
            +
                      end
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                      # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L106
         | 
| 137 | 
            +
                      def loop?(ancestor, node)
         | 
| 138 | 
            +
                        keyword_loop?(ancestor.type) ||
         | 
| 139 | 
            +
                          kernel_loop?(ancestor) ||
         | 
| 140 | 
            +
                          node_within_enumerable_loop?(node, ancestor) ||
         | 
| 141 | 
            +
                          csv_loop?(ancestor)
         | 
| 142 | 
            +
                      end
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                      # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L112
         | 
| 145 | 
            +
                      def keyword_loop?(type)
         | 
| 146 | 
            +
                        LOOP_TYPES.include?(type)
         | 
| 147 | 
            +
                      end
         | 
| 148 | 
            +
             | 
| 149 | 
            +
                      # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L116
         | 
| 150 | 
            +
                      def node_within_enumerable_loop?(node, ancestor)
         | 
| 151 | 
            +
                        enumerable_loop?(ancestor) do |receiver|
         | 
| 152 | 
            +
                          receiver != node && !receiver&.descendants&.include?(node)
         | 
| 153 | 
            +
                        end
         | 
| 154 | 
            +
                      end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                      # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L130
         | 
| 157 | 
            +
                      def enumerable_method?(method_name)
         | 
| 158 | 
            +
                        ENUMERABLE_METHOD_NAMES.include?(method_name)
         | 
| 159 | 
            +
                      end
         | 
| 160 | 
            +
             | 
| 161 | 
            +
                      # @param corrector [RuboCop::Cop::Corrector]
         | 
| 162 | 
            +
                      # @param current_node [RuboCop::AST::Node]
         | 
| 163 | 
            +
                      # @param parent_node [RuboCop::AST::Node]
         | 
| 164 | 
            +
                      # @param type [Symbol] Node type. one of `:str`, `:dstr`
         | 
| 165 | 
            +
                      # @param gda [RuboCop::Isucon::GDA::Client]
         | 
| 166 | 
            +
                      def perform_autocorrect(corrector:, current_node:, parent_node:, type:, gda:)
         | 
| 167 | 
            +
                        return unless enabled_database?
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                        corrector = Correctors::Mysql2NPlusOneQueryCorrector.new(
         | 
| 170 | 
            +
                          corrector: corrector, current_node: current_node,
         | 
| 171 | 
            +
                          parent_node: parent_node, type: type, gda: gda, connection: connection
         | 
| 172 | 
            +
                        )
         | 
| 173 | 
            +
                        corrector.correct
         | 
| 174 | 
            +
                      end
         | 
| 175 | 
            +
                    end
         | 
| 176 | 
            +
                  end
         | 
| 177 | 
            +
                end
         | 
| 178 | 
            +
              end
         | 
| 179 | 
            +
            end
         |