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