rubocop-isucon 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/gh-pages.yml +44 -0
  3. data/.github/workflows/test.yml +91 -0
  4. data/.gitignore +13 -0
  5. data/.rspec +3 -0
  6. data/.rubocop.yml +43 -0
  7. data/.yardopts +7 -0
  8. data/CHANGELOG.md +6 -0
  9. data/Gemfile +8 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +108 -0
  12. data/Rakefile +35 -0
  13. data/benchmark/README.md +69 -0
  14. data/benchmark/memorize.rb +86 -0
  15. data/benchmark/parse_table.rb +103 -0
  16. data/benchmark/shell.rb +26 -0
  17. data/bin/console +15 -0
  18. data/bin/setup +8 -0
  19. data/config/default.yml +83 -0
  20. data/config/enable-only-performance.yml +30 -0
  21. data/gemfiles/activerecord_6_1.gemfile +14 -0
  22. data/gemfiles/activerecord_7_0.gemfile +14 -0
  23. data/lib/rubocop/cop/isucon/correctors/mysql2_n_plus_one_query_corrector/correctable_methods.rb +66 -0
  24. data/lib/rubocop/cop/isucon/correctors/mysql2_n_plus_one_query_corrector/replace_methods.rb +127 -0
  25. data/lib/rubocop/cop/isucon/correctors/mysql2_n_plus_one_query_corrector.rb +112 -0
  26. data/lib/rubocop/cop/isucon/mixin/database_methods.rb +59 -0
  27. data/lib/rubocop/cop/isucon/mixin/mysql2_xquery_methods.rb +176 -0
  28. data/lib/rubocop/cop/isucon/mixin/sinatra_methods.rb +37 -0
  29. data/lib/rubocop/cop/isucon/mysql2/join_without_index.rb +100 -0
  30. data/lib/rubocop/cop/isucon/mysql2/many_join_table.rb +86 -0
  31. data/lib/rubocop/cop/isucon/mysql2/n_plus_one_query.rb +179 -0
  32. data/lib/rubocop/cop/isucon/mysql2/prepare_execute.rb +136 -0
  33. data/lib/rubocop/cop/isucon/mysql2/select_asterisk.rb +171 -0
  34. data/lib/rubocop/cop/isucon/mysql2/where_without_index.rb +105 -0
  35. data/lib/rubocop/cop/isucon/shell/backtick.rb +36 -0
  36. data/lib/rubocop/cop/isucon/shell/system.rb +36 -0
  37. data/lib/rubocop/cop/isucon/sinatra/disable_logging.rb +83 -0
  38. data/lib/rubocop/cop/isucon/sinatra/logger.rb +52 -0
  39. data/lib/rubocop/cop/isucon/sinatra/rack_logger.rb +58 -0
  40. data/lib/rubocop/cop/isucon/sinatra/serve_static_file.rb +73 -0
  41. data/lib/rubocop/cop/isucon_cops.rb +20 -0
  42. data/lib/rubocop/isucon/database_connection.rb +42 -0
  43. data/lib/rubocop/isucon/gda/client.rb +184 -0
  44. data/lib/rubocop/isucon/gda/gda_ext.rb +119 -0
  45. data/lib/rubocop/isucon/gda/join_condition.rb +25 -0
  46. data/lib/rubocop/isucon/gda/join_operand.rb +46 -0
  47. data/lib/rubocop/isucon/gda/node_location.rb +42 -0
  48. data/lib/rubocop/isucon/gda/node_patcher.rb +101 -0
  49. data/lib/rubocop/isucon/gda/where_condition.rb +73 -0
  50. data/lib/rubocop/isucon/gda/where_operand.rb +32 -0
  51. data/lib/rubocop/isucon/gda.rb +28 -0
  52. data/lib/rubocop/isucon/inject.rb +20 -0
  53. data/lib/rubocop/isucon/memorize_methods.rb +38 -0
  54. data/lib/rubocop/isucon/version.rb +7 -0
  55. data/lib/rubocop/isucon.rb +20 -0
  56. data/lib/rubocop-isucon.rb +16 -0
  57. data/rubocop-isucon.gemspec +52 -0
  58. metadata +286 -0
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Isucon
6
+ module Sinatra
7
+ # Serve static files on front server (e.g. nginx) instead of sinatra app
8
+ #
9
+ # @example
10
+ # # bad
11
+ # class App < Sinatra::Base
12
+ # get '/' do
13
+ # content_type :html
14
+ # File.read(File.join(__dir__, '..', 'public', 'index.html'))
15
+ # end
16
+ # end
17
+ #
18
+ # # good (e.g. Serve on nginx)
19
+ # location / {
20
+ # try_files $uri $uri/ /index.html;
21
+ # }
22
+ #
23
+ class ServeStaticFile < Base
24
+ include Mixin::SinatraMethods
25
+
26
+ MSG = "Serve static files on front server (e.g. nginx) instead of sinatra app"
27
+
28
+ # @!method file_read_method?(node)
29
+ # @param node [RuboCop::AST::Node]
30
+ # @return [Boolean]
31
+ def_node_matcher :file_read_method?, <<~PATTERN
32
+ (send (const nil? :File) :read ...)
33
+ PATTERN
34
+
35
+ # @!method get_block?(node)
36
+ # @param node [RuboCop::AST::Node]
37
+ # @return [Boolean]
38
+ def_node_matcher :get_block?, <<~PATTERN
39
+ (block (send nil? :get ...) ...)
40
+ PATTERN
41
+
42
+ # @param node [RuboCop::AST::Node]
43
+ def on_send(node)
44
+ return unless parent_is_sinatra_app?(node)
45
+ return unless file_read_method?(node)
46
+
47
+ parent = parent_get_node(node)
48
+ return unless parent
49
+
50
+ return unless end_of_block?(node: node, parent: parent)
51
+
52
+ add_offense(parent)
53
+ end
54
+
55
+ private
56
+
57
+ # @param node [RuboCop::AST::Node]
58
+ # @return [RuboCop::AST::Node]
59
+ def parent_get_node(node)
60
+ node.each_ancestor.find { |ancestor| get_block?(ancestor) }
61
+ end
62
+
63
+ # @param node [RuboCop::AST::Node]
64
+ # @param parent [RuboCop::AST::Node]
65
+ # @return [Boolean]
66
+ def end_of_block?(node:, parent:)
67
+ parent.child_nodes.last&.child_nodes&.last == node
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "isucon/mixin/database_methods"
4
+ require_relative "isucon/mixin/mysql2_xquery_methods"
5
+ require_relative "isucon/mixin/sinatra_methods"
6
+
7
+ require_relative "isucon/correctors/mysql2_n_plus_one_query_corrector"
8
+
9
+ require_relative "isucon/mysql2/join_without_index"
10
+ require_relative "isucon/mysql2/many_join_table"
11
+ require_relative "isucon/mysql2/n_plus_one_query"
12
+ require_relative "isucon/mysql2/prepare_execute"
13
+ require_relative "isucon/mysql2/select_asterisk"
14
+ require_relative "isucon/mysql2/where_without_index"
15
+ require_relative "isucon/sinatra/disable_logging"
16
+ require_relative "isucon/sinatra/logger"
17
+ require_relative "isucon/sinatra/rack_logger"
18
+ require_relative "isucon/sinatra/serve_static_file"
19
+ require_relative "isucon/shell/backtick"
20
+ require_relative "isucon/shell/system"
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Isucon
5
+ # Manage database connection
6
+ class DatabaseConnection
7
+ # @param database_config [Hash] Same as `ActiveRecord::Base.establish_connection` argument
8
+ # @see https://api.rubyonrails.org/classes/ActiveRecord/ConnectionHandling.html#method-i-establish_connection
9
+ def initialize(database_config)
10
+ ActiveRecord::Base.establish_connection(database_config)
11
+ @column_names_by_table = {}
12
+ @indexes_by_table = {}
13
+ @primary_keys_by_table = {}
14
+ end
15
+
16
+ # @param table_name [String]
17
+ # @return [Array<String>]
18
+ def column_names(table_name)
19
+ @column_names_by_table[table_name] ||= ActiveRecord::Base.connection.columns(table_name).map(&:name)
20
+ end
21
+
22
+ # @param table_name [String]
23
+ # @return [Array<ActiveRecord::ConnectionAdapters::IndexDefinition>]
24
+ # @see https://github.com/rails/rails/blob/v6.1.4.1/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L8
25
+ def indexes(table_name)
26
+ @indexes_by_table[table_name] ||= ActiveRecord::Base.connection.indexes(table_name)
27
+ end
28
+
29
+ # @param table_name [String]
30
+ # @return [Array<Array<String>>] column names of indexes
31
+ def unique_index_columns(table_name)
32
+ indexes(table_name).select(&:unique).map(&:columns)
33
+ end
34
+
35
+ # @param table_name [String]
36
+ # @return [Array<String>] primary key's column names
37
+ def primary_keys(table_name)
38
+ @primary_keys_by_table[table_name] ||= ActiveRecord::Base.connection.primary_keys(table_name)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Isucon
5
+ module GDA
6
+ # Client for `GDA`
7
+ class Client # rubocop:disable Metrics/ClassLength
8
+ # @return [GDA::Nodes::Select]
9
+ attr_reader :ast
10
+
11
+ # @return [String]
12
+ attr_reader :sql
13
+
14
+ # @param sql [String,nil]
15
+ # @param ast [GDA::Nodes::Select]
16
+ # @note if `sql` is `nil`, `ast` is required
17
+ def initialize(sql, ast: nil)
18
+ @sql = sql
19
+
20
+ if ast
21
+ # called from subquery AST
22
+ @ast = ast
23
+ else
24
+ # called from root AST
25
+ @ast = statement.ast
26
+ RuboCop::Isucon::GDA::NodePatcher.new(sql).accept(@ast)
27
+ end
28
+ end
29
+
30
+ # @return [Array<String>]
31
+ def table_names
32
+ return @table_names if @table_names
33
+
34
+ @table_names =
35
+ if from_targets?
36
+ ast.from.targets.map(&:table_name).compact.uniq
37
+ else
38
+ []
39
+ end
40
+ end
41
+
42
+ # @return [Array<RuboCop::Isucon::GDA::WhereCondition>]
43
+ def where_conditions
44
+ where_nodes.
45
+ map do |node|
46
+ where_operands = node.operands.map do |operand|
47
+ create_where_operand(operand)
48
+ end
49
+
50
+ WhereCondition.new(
51
+ operator: node.operator,
52
+ operands: where_operands,
53
+ )
54
+ end
55
+ end
56
+
57
+ # @return [Array<RuboCop::Isucon::GDA::JoinCondition>]
58
+ def join_conditions
59
+ return [] unless from_joins?
60
+
61
+ ast.from.joins.map do |node|
62
+ join_operands = node.expr.cond.operands.map do |operand|
63
+ create_join_operand(operand)
64
+ end
65
+
66
+ JoinCondition.new(
67
+ operator: node.expr.cond.operator,
68
+ operands: join_operands,
69
+ )
70
+ end
71
+ end
72
+
73
+ # @return [Array<GDA::Nodes::Operation>]
74
+ def where_nodes
75
+ return [] unless ast.respond_to?(:where_cond)
76
+
77
+ ast.where_cond.to_a.
78
+ select { |node| node.instance_of?(::GDA::Nodes::Operation) && node.operator }
79
+ end
80
+
81
+ # @return [Hash,nil]
82
+ def serialize_statement
83
+ return nil unless @sql
84
+
85
+ JSON.parse(statement.serialize)
86
+ end
87
+
88
+ # @yieldparam gda [RuboCop::Isucon::GDA::Client]
89
+ def visit_subquery_recursive(&block)
90
+ return unless from_targets?
91
+
92
+ ast.from.targets.each do |target|
93
+ next unless target.expr.select
94
+
95
+ gda = Client.new(nil, ast: target.expr.select)
96
+ yield(gda)
97
+ gda.visit_subquery_recursive(&block)
98
+ end
99
+ end
100
+
101
+ # @yieldparam gda [RuboCop::Isucon::GDA::Client]
102
+ def visit_all(&block)
103
+ yield(self)
104
+ visit_subquery_recursive(&block)
105
+ end
106
+
107
+ # @return [Boolean]
108
+ def select_query?
109
+ ast.is_a?(::GDA::Nodes::Select)
110
+ end
111
+
112
+ # Whether `SELECT` clause contains aggregate functions (`COUNT`, `MAX`, `MIN`, `SUM` or `AVG`)
113
+ # @return [Boolean]
114
+ def contains_aggregate_functions?
115
+ aggregate_function_names = %w[COUNT MAX MIN SUM AVG]
116
+ ast.expr_list.any? do |select_field_node|
117
+ aggregate_function_names.include?(select_field_node.expr.func&.function_name&.upcase)
118
+ end
119
+ end
120
+
121
+ # Whether AST has `GROUP BY` clause
122
+ # @return [Boolean]
123
+ def group_by_clause?
124
+ !ast.group_by.empty?
125
+ end
126
+
127
+ # Whether AST has `LIMIT` clause
128
+ # @return [Boolean]
129
+ def limit_clause?
130
+ !!ast.limit_count
131
+ end
132
+
133
+ # @return [Boolean]
134
+ def from_joins?
135
+ ast.respond_to?(:from) && ast.from.respond_to?(:joins)
136
+ end
137
+
138
+ # @return [Boolean]
139
+ def from_targets?
140
+ ast.respond_to?(:from) && ast.from.respond_to?(:targets)
141
+ end
142
+
143
+ private
144
+
145
+ # @return [GDA::SQL::Statement]
146
+ # @raise [ArgumentError] called from subquery
147
+ def statement
148
+ return @statement if @statement
149
+
150
+ raise ArgumentError, "@sql is required" unless @sql
151
+
152
+ @statement = ::GDA::SQL::Parser.new.parse(RuboCop::Isucon::GDA.normalize_sql(@sql))
153
+ end
154
+
155
+ # @param operand [GDA::Nodes::Expr]
156
+ # @return [RuboCop::Isucon::GDA::WhereOperand]
157
+ def create_where_operand(operand)
158
+ WhereOperand.new(value: operand.value.gsub(/^.+\./, ""), node: operand)
159
+ end
160
+
161
+ # @param operand [GDA::Nodes::Expr]
162
+ # @return [RuboCop::Isucon::GDA::JoinOperand]
163
+ def create_join_operand(operand)
164
+ table_name_or_as, column_name = operand.value.split(".", 2)
165
+
166
+ if (target = from_targets.find { |t| table_name_or_as == t[:table_name] })
167
+ return JoinOperand.new(table_name: target[:table_name], column_name: column_name, as: nil, node: operand)
168
+ end
169
+
170
+ if (target = from_targets.find { |t| table_name_or_as == t[:as] })
171
+ return JoinOperand.new(table_name: target[:table_name], column_name: column_name, as: target[:as], node: operand)
172
+ end
173
+
174
+ JoinOperand.new(table_name: nil, column_name: column_name, as: nil, node: operand)
175
+ end
176
+
177
+ # @return [Hash]
178
+ def from_targets
179
+ @from_targets ||= ast.from.targets.map { |target| { table_name: target.table_name, as: target.as } }
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ GDA::Nodes::Node.class_eval do
4
+ # @!attribute [rw] location
5
+ # @return [RuboCop::Isucon::GDA::NodeLocation]
6
+ attr_accessor :location
7
+
8
+ # @return [String]
9
+ def inspect
10
+ # NOTE: Suppress the inclusion of instance variables in `#inspect`
11
+ encoded_object_id = super[/#<#{self.class.name}:0x([0-9a-z]{16})/, 1]
12
+ "#<#{self.class.name}:0x#{encoded_object_id}>"
13
+ end
14
+ end
15
+
16
+ GDA::Nodes::Select.class_eval do
17
+ extend RuboCop::Isucon::MemorizeMethods
18
+
19
+ memorize :distinct_expr
20
+ memorize :expr_list
21
+ memorize :from
22
+ memorize :where_cond
23
+ memorize :group_by
24
+ memorize :having_cond
25
+ memorize :order_by
26
+ memorize :limit_count
27
+ memorize :limit_offset
28
+ end
29
+
30
+ GDA::Nodes::Insert.class_eval do
31
+ extend RuboCop::Isucon::MemorizeMethods
32
+
33
+ memorize :table
34
+ memorize :fields_list
35
+ memorize :values_list
36
+ memorize :select
37
+ end
38
+
39
+ GDA::Nodes::Update.class_eval do
40
+ extend RuboCop::Isucon::MemorizeMethods
41
+
42
+ memorize :table
43
+ memorize :fields_list
44
+ memorize :expr_list
45
+ memorize :cond
46
+ end
47
+
48
+ GDA::Nodes::Join.class_eval do
49
+ extend RuboCop::Isucon::MemorizeMethods
50
+
51
+ memorize :expr
52
+ memorize :use
53
+ end
54
+
55
+ GDA::Nodes::Delete.class_eval do
56
+ extend RuboCop::Isucon::MemorizeMethods
57
+
58
+ memorize :table
59
+ memorize :cond
60
+ end
61
+
62
+ GDA::Nodes::SelectField.class_eval do
63
+ extend RuboCop::Isucon::MemorizeMethods
64
+
65
+ memorize :expr
66
+ end
67
+
68
+ GDA::Nodes::Expr.class_eval do
69
+ extend RuboCop::Isucon::MemorizeMethods
70
+
71
+ memorize :func
72
+ memorize :cond
73
+ memorize :select
74
+ memorize :case_s
75
+ memorize :param_spec
76
+ end
77
+
78
+ GDA::Nodes::From.class_eval do
79
+ extend RuboCop::Isucon::MemorizeMethods
80
+
81
+ memorize :targets
82
+ memorize :joins
83
+ end
84
+
85
+ GDA::Nodes::Target.class_eval do
86
+ extend RuboCop::Isucon::MemorizeMethods
87
+
88
+ memorize :expr
89
+ end
90
+
91
+ GDA::Nodes::Operation.class_eval do
92
+ extend RuboCop::Isucon::MemorizeMethods
93
+
94
+ memorize :operands
95
+ end
96
+
97
+ GDA::Nodes::Function.class_eval do
98
+ extend RuboCop::Isucon::MemorizeMethods
99
+
100
+ memorize :args_list
101
+ end
102
+
103
+ GDA::Nodes::Order.class_eval do
104
+ extend RuboCop::Isucon::MemorizeMethods
105
+
106
+ memorize :expr
107
+ end
108
+
109
+ GDA::Nodes::Unknown.class_eval do
110
+ extend RuboCop::Isucon::MemorizeMethods
111
+
112
+ memorize :expressions
113
+ end
114
+
115
+ GDA::Nodes::Compound.class_eval do
116
+ extend RuboCop::Isucon::MemorizeMethods
117
+
118
+ memorize :stmt_list
119
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Isucon
5
+ module GDA
6
+ # response of {RuboCop::Isucon::GDA::Client#join_conditions}
7
+ class JoinCondition
8
+ # @!attribute [rw] operator
9
+ # @return [String]
10
+ attr_accessor :operator
11
+
12
+ # @!attribute [rw] operands
13
+ # @return [Array<RuboCop::Isucon::GDA::JoinOperand>]
14
+ attr_accessor :operands
15
+
16
+ # @param operator [String]
17
+ # @param operands [Array<RuboCop::Isucon::GDA::JoinOperand>]
18
+ def initialize(operator: nil, operands: [])
19
+ @operator = operator
20
+ @operands = operands
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Isucon
5
+ module GDA
6
+ # response of {RuboCop::Isucon::GDA::Client#join_conditions}
7
+ class JoinOperand
8
+ # @!attribute [rw] table_name
9
+ # @return [String]
10
+ attr_accessor :table_name
11
+
12
+ # @!attribute [rw] column_name
13
+ # @return [String]
14
+ attr_accessor :column_name
15
+
16
+ # @!attribute [rw] as
17
+ # @return [String]
18
+ attr_accessor :as
19
+
20
+ # @!attribute [rw] node
21
+ # @return [GDA::Nodes::Expr]
22
+ attr_accessor :node
23
+
24
+ # @param table_name [String]
25
+ # @param column_name [String]
26
+ # @param as [String]
27
+ # @param node [GDA::Nodes::Expr]
28
+ def initialize(table_name: nil, column_name: nil, as: nil, node: nil) # rubocop:disable Naming/MethodParameterName
29
+ @table_name = table_name
30
+ @column_name = column_name
31
+ @as = as
32
+ @node = node
33
+ end
34
+
35
+ # @param other [RuboCop::Isucon::GDA::JoinOperand]
36
+ # @return [Boolean]
37
+ def ==(other)
38
+ other.is_a?(JoinOperand) &&
39
+ table_name == other.table_name &&
40
+ column_name == other.column_name &&
41
+ as == other.as
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Isucon
5
+ module GDA
6
+ # Location in SQL
7
+ class NodeLocation
8
+ # @return [Integer]
9
+ attr_reader :begin_pos
10
+
11
+ # @return [Integer]
12
+ attr_reader :end_pos
13
+
14
+ # @return [String]
15
+ attr_reader :body
16
+
17
+ # @param begin_pos [Integer]
18
+ # @param end_pos [Integer]
19
+ # @param body [String]
20
+ def initialize(begin_pos:, end_pos:, body:)
21
+ @begin_pos = begin_pos
22
+ @end_pos = end_pos
23
+ @body = body
24
+ end
25
+
26
+ # @param other [RuboCop::Isucon::GDA::NodeLocation]
27
+ # @return [Boolean]
28
+ def ==(other)
29
+ other.is_a?(NodeLocation) &&
30
+ begin_pos == other.begin_pos &&
31
+ end_pos == other.end_pos &&
32
+ body == other.body
33
+ end
34
+
35
+ # @return [Integer]
36
+ def length
37
+ end_pos - begin_pos
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Isucon
5
+ module GDA
6
+ # Monkey patching to `GDA::Nodes::Node`
7
+ class NodePatcher < ::GDA::Visitors::Visitor
8
+ # @param sql [String]
9
+ def initialize(sql)
10
+ @sql = sql
11
+ @normalized_sql = RuboCop::Isucon::GDA.normalize_sql(sql)
12
+ @current_operation_pos = 0
13
+ @current_expr_pos = 0
14
+ super()
15
+ end
16
+
17
+ private
18
+
19
+ # @param node [GDA::Nodes::Operation]
20
+ def visit_GDA_Nodes_Operation(node) # rubocop:disable Naming/MethodName -- This method is called from `GDA::Visitors::Visitor#visit` c.f. https://github.com/tenderlove/gda/blob/v1.1.0/lib/gda/visitors/visitor.rb#L13-L17
21
+ return super unless node.operator
22
+
23
+ pattern = operand_pattern(node)
24
+ return super unless pattern
25
+
26
+ node.location = search_operation_location(pattern)
27
+
28
+ super
29
+ end
30
+
31
+ # @param node [GDA::Nodes::Operation]
32
+ # @return [Regexp,nil]
33
+ def operand_pattern(node)
34
+ operand0 = Regexp.escape(node.operands[0].value)
35
+ operator = Regexp.escape(node.operator)
36
+
37
+ case node.operands.count
38
+ when 1
39
+ /#{operand0}\s*#{operator}/
40
+ when 2
41
+ /#{operand0}\s*#{operator}\s*#{Regexp.escape(node.operands[1].value)}/
42
+ end
43
+ end
44
+
45
+ # @param pattern [Regexp]
46
+ # @return [RuboCop::Isucon::GDA::NodeLocation,nil]
47
+ def search_operation_location(pattern)
48
+ result = search_location(pattern, @current_operation_pos)
49
+ return nil unless result
50
+
51
+ @current_operation_pos = result[:current_pos] if result[:current_pos]
52
+ result[:location]
53
+ end
54
+
55
+ # @param pattern [Regexp]
56
+ # @param current_pos [Integer]
57
+ # @return [Hash]
58
+ def search_location(pattern, current_pos)
59
+ begin_pos = @normalized_sql.index(pattern, current_pos)
60
+
61
+ return nil unless Regexp.last_match
62
+
63
+ length = Regexp.last_match[0].length
64
+ end_pos = begin_pos + length
65
+
66
+ begin_pos -= 1 if @sql[begin_pos - 1] == "`"
67
+ end_pos += 1 if @sql[end_pos] == "`"
68
+
69
+ {
70
+ location: NodeLocation.new(begin_pos: begin_pos, end_pos: end_pos, body: @sql[begin_pos...end_pos]),
71
+ current_pos: end_pos,
72
+ }
73
+ end
74
+
75
+ # @param node [GDA::Nodes::Expr]
76
+ def visit_GDA_Nodes_Expr(node) # rubocop:disable Naming/MethodName -- This method is called from `GDA::Visitors::Visitor#visit` c.f. https://github.com/tenderlove/gda/blob/v1.1.0/lib/gda/visitors/visitor.rb#L13-L17
77
+ return super unless node.value
78
+
79
+ escaped_value = Regexp.escape(node.value).gsub("\\.", "\\s*\\.\\s*")
80
+ node.location = search_expr_location(/(?<=[\s,])#{escaped_value}(?=[\s,])/)
81
+
82
+ begin
83
+ super
84
+ rescue RuntimeError => e
85
+ raise unless e.message.strip == "unknown node type: 505"
86
+ end
87
+ end
88
+
89
+ # @param pattern [Regexp]
90
+ # @return [RuboCop::Isucon::GDA::NodeLocation,nil]
91
+ def search_expr_location(pattern)
92
+ result = search_location(pattern, @current_expr_pos)
93
+ return nil unless result
94
+
95
+ @current_expr_pos = result[:current_pos] if result[:current_pos]
96
+ result[:location]
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end