search_cop 1.0.6 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +42 -0
- data/.rubocop.yml +128 -0
- data/CHANGELOG.md +36 -5
- data/CONTRIBUTING.md +18 -0
- data/Gemfile +4 -17
- data/README.md +143 -35
- data/Rakefile +0 -1
- data/docker-compose.yml +18 -0
- data/gemfiles/rails5.gemfile +13 -0
- data/gemfiles/rails6.gemfile +13 -0
- data/lib/search_cop.rb +15 -13
- data/lib/search_cop/grammar_parser.rb +3 -4
- data/lib/search_cop/hash_parser.rb +23 -17
- data/lib/search_cop/helpers.rb +15 -0
- data/lib/search_cop/query_builder.rb +2 -4
- data/lib/search_cop/query_info.rb +0 -2
- data/lib/search_cop/search_scope.rb +8 -5
- data/lib/search_cop/version.rb +1 -1
- data/lib/search_cop/visitors.rb +0 -2
- data/lib/search_cop/visitors/mysql.rb +9 -7
- data/lib/search_cop/visitors/postgres.rb +18 -8
- data/lib/search_cop/visitors/visitor.rb +13 -6
- data/lib/search_cop_grammar.rb +18 -9
- data/lib/search_cop_grammar.treetop +6 -4
- data/lib/search_cop_grammar/attributes.rb +77 -34
- data/lib/search_cop_grammar/nodes.rb +7 -2
- data/search_cop.gemspec +9 -10
- data/test/and_test.rb +6 -8
- data/test/boolean_test.rb +7 -9
- data/test/database.yml +4 -1
- data/test/date_test.rb +38 -12
- data/test/datetime_test.rb +45 -12
- data/test/default_operator_test.rb +51 -0
- data/test/error_test.rb +2 -4
- data/test/float_test.rb +16 -11
- data/test/fulltext_test.rb +8 -10
- data/test/hash_test.rb +39 -31
- data/test/integer_test.rb +9 -11
- data/test/not_test.rb +6 -8
- data/test/or_test.rb +8 -10
- data/test/scope_test.rb +11 -13
- data/test/search_cop_test.rb +36 -34
- data/test/string_test.rb +67 -19
- data/test/test_helper.rb +24 -18
- data/test/visitor_test.rb +15 -8
- metadata +41 -55
- data/.travis.yml +0 -34
- data/Appraisals +0 -20
- data/gemfiles/3.2.gemfile +0 -26
- data/gemfiles/4.0.gemfile +0 -26
- data/gemfiles/4.1.gemfile +0 -26
- data/gemfiles/4.2.gemfile +0 -26
data/lib/search_cop_grammar.rb
CHANGED
@@ -1,13 +1,16 @@
|
|
1
|
-
|
2
1
|
require "search_cop_grammar/attributes"
|
3
2
|
require "search_cop_grammar/nodes"
|
4
3
|
|
5
4
|
module SearchCopGrammar
|
6
5
|
class BaseNode < Treetop::Runtime::SyntaxNode
|
7
|
-
|
6
|
+
attr_writer :query_info, :query_options
|
8
7
|
|
9
8
|
def query_info
|
10
|
-
@query_info || parent.query_info
|
9
|
+
(@query_info ||= nil) || parent.query_info
|
10
|
+
end
|
11
|
+
|
12
|
+
def query_options
|
13
|
+
(@query_options ||= nil) || parent.query_options
|
11
14
|
end
|
12
15
|
|
13
16
|
def evaluate
|
@@ -15,7 +18,7 @@ module SearchCopGrammar
|
|
15
18
|
end
|
16
19
|
|
17
20
|
def elements
|
18
|
-
super.
|
21
|
+
super.reject { |element| element.instance_of?(Treetop::Runtime::SyntaxNode) }
|
19
22
|
end
|
20
23
|
|
21
24
|
def collection_for(key)
|
@@ -94,13 +97,13 @@ module SearchCopGrammar
|
|
94
97
|
|
95
98
|
class SingleQuotedAnywhereExpression < AnywhereExpression
|
96
99
|
def text_value
|
97
|
-
super.gsub
|
100
|
+
super.gsub(/^'|'$/, "")
|
98
101
|
end
|
99
102
|
end
|
100
103
|
|
101
104
|
class DoubleQuotedAnywhereExpression < AnywhereExpression
|
102
105
|
def text_value
|
103
|
-
super.gsub
|
106
|
+
super.gsub(/^"|"$/, "")
|
104
107
|
end
|
105
108
|
end
|
106
109
|
|
@@ -110,6 +113,13 @@ module SearchCopGrammar
|
|
110
113
|
end
|
111
114
|
end
|
112
115
|
|
116
|
+
class AndOrExpression < BaseNode
|
117
|
+
def evaluate
|
118
|
+
default_operator = SearchCop::Helpers.sanitize_default_operator(query_options)
|
119
|
+
[elements.first.evaluate, elements.last.evaluate].inject(default_operator)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
113
123
|
class OrExpression < BaseNode
|
114
124
|
def evaluate
|
115
125
|
[elements.first.evaluate, elements.last.evaluate].inject(:or)
|
@@ -130,16 +140,15 @@ module SearchCopGrammar
|
|
130
140
|
|
131
141
|
class SingleQuotedValue < BaseNode
|
132
142
|
def text_value
|
133
|
-
super.gsub
|
143
|
+
super.gsub(/^'|'$/, "")
|
134
144
|
end
|
135
145
|
end
|
136
146
|
|
137
147
|
class DoubleQuotedValue < BaseNode
|
138
148
|
def text_value
|
139
|
-
super.gsub
|
149
|
+
super.gsub(/^"|"$/, "")
|
140
150
|
end
|
141
151
|
end
|
142
152
|
|
143
153
|
class Value < BaseNode; end
|
144
154
|
end
|
145
|
-
|
@@ -9,7 +9,9 @@ grammar SearchCopGrammar
|
|
9
9
|
end
|
10
10
|
|
11
11
|
rule and_expression
|
12
|
-
or_expression
|
12
|
+
or_expression space? ('AND' / 'and') space? complex_expression <AndExpression> /
|
13
|
+
or_expression space !('OR' / 'or') complex_expression <AndOrExpression> /
|
14
|
+
or_expression
|
13
15
|
end
|
14
16
|
|
15
17
|
rule or_expression
|
@@ -37,7 +39,7 @@ grammar SearchCopGrammar
|
|
37
39
|
end
|
38
40
|
|
39
41
|
rule anywhere_expression
|
40
|
-
"'" [^\']* "'" <SingleQuotedAnywhereExpression> / '"' [^\"]* '"' <DoubleQuotedAnywhereExpression> / [
|
42
|
+
"'" [^\']* "'" <SingleQuotedAnywhereExpression> / '"' [^\"]* '"' <DoubleQuotedAnywhereExpression> / [^[:blank:]()]+ <AnywhereExpression>
|
41
43
|
end
|
42
44
|
|
43
45
|
rule simple_column
|
@@ -45,11 +47,11 @@ grammar SearchCopGrammar
|
|
45
47
|
end
|
46
48
|
|
47
49
|
rule value
|
48
|
-
"'" [^\']* "'" <SingleQuotedValue> / '"' [^\"]* '"' <DoubleQuotedValue> / [
|
50
|
+
"'" [^\']* "'" <SingleQuotedValue> / '"' [^\"]* '"' <DoubleQuotedValue> / [^[:blank:]()]+ <Value>
|
49
51
|
end
|
50
52
|
|
51
53
|
rule space
|
52
|
-
[
|
54
|
+
[[:blank:]]+
|
53
55
|
end
|
54
56
|
end
|
55
57
|
|
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
require "treetop"
|
3
2
|
|
4
3
|
module SearchCopGrammar
|
@@ -6,6 +5,8 @@ module SearchCopGrammar
|
|
6
5
|
class Collection
|
7
6
|
attr_reader :query_info, :key
|
8
7
|
|
8
|
+
INCLUDED_OPERATORS = [:matches, :eq, :not_eq, :gt, :gteq, :lt, :lteq].freeze
|
9
|
+
|
9
10
|
def initialize(query_info, key)
|
10
11
|
raise(SearchCop::UnknownColumn, "Unknown column #{key}") unless query_info.scope.reflection.attributes[key]
|
11
12
|
|
@@ -26,9 +27,15 @@ module SearchCopGrammar
|
|
26
27
|
end
|
27
28
|
|
28
29
|
[:eq, :not_eq, :lt, :lteq, :gt, :gteq].each do |method|
|
29
|
-
|
30
|
-
|
31
|
-
|
30
|
+
define_method method do |value|
|
31
|
+
attributes.collect! { |attribute| attribute.send method, value }.inject(:or)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def generator(generator, value)
|
36
|
+
attributes.collect! do |attribute|
|
37
|
+
SearchCopGrammar::Nodes::Generator.new(attribute, generator: generator, value: value)
|
38
|
+
end.inject(:or)
|
32
39
|
end
|
33
40
|
|
34
41
|
def matches(value)
|
@@ -88,6 +95,18 @@ module SearchCopGrammar
|
|
88
95
|
|
89
96
|
Attributes.const_get(klass.columns_hash[column].type.to_s.classify).new(klass, alias_for(table), column, options)
|
90
97
|
end
|
98
|
+
|
99
|
+
def generator_for(name)
|
100
|
+
generators[name]
|
101
|
+
end
|
102
|
+
|
103
|
+
def valid_operator?(operator)
|
104
|
+
(INCLUDED_OPERATORS + generators.keys).include?(operator)
|
105
|
+
end
|
106
|
+
|
107
|
+
def generators
|
108
|
+
query_info.scope.reflection.generators
|
109
|
+
end
|
91
110
|
end
|
92
111
|
|
93
112
|
class Base
|
@@ -117,7 +136,7 @@ module SearchCopGrammar
|
|
117
136
|
false
|
118
137
|
end
|
119
138
|
|
120
|
-
{ :
|
139
|
+
{ eq: "Equality", not_eq: "NotEqual", lt: "LessThan", lteq: "LessThanOrEqual", gt: "GreaterThan", gteq: "GreaterThanOrEqual", matches: "Matches" }.each do |method, class_name|
|
121
140
|
define_method method do |value|
|
122
141
|
raise(SearchCop::IncompatibleDatatype, "Incompatible datatype for #{value}") unless compatible?(value)
|
123
142
|
|
@@ -126,19 +145,32 @@ module SearchCopGrammar
|
|
126
145
|
end
|
127
146
|
|
128
147
|
def method_missing(name, *args, &block)
|
129
|
-
@attribute.
|
148
|
+
if @attribute.respond_to?(name)
|
149
|
+
@attribute.send(name, *args, &block)
|
150
|
+
else
|
151
|
+
super
|
152
|
+
end
|
130
153
|
end
|
131
154
|
|
132
|
-
def
|
133
|
-
|
155
|
+
def respond_to_missing?(*args)
|
156
|
+
@attribute.respond_to?(*args) || super
|
134
157
|
end
|
135
158
|
end
|
136
159
|
|
137
160
|
class String < Base
|
138
161
|
def matches_value(value)
|
139
|
-
|
162
|
+
res = value.gsub(/[%_\\]/) { |char| "\\#{char}" }
|
163
|
+
|
164
|
+
if value.strip =~ /^\*|\*$/
|
165
|
+
res = res.gsub(/^\*/, "%") if options[:left_wildcard] != false
|
166
|
+
res = res.gsub(/\*$/, "%") if options[:right_wildcard] != false
|
167
|
+
|
168
|
+
return res
|
169
|
+
end
|
140
170
|
|
141
|
-
|
171
|
+
res = "%#{res}" if options[:left_wildcard] != false
|
172
|
+
res = "#{res}%" if options[:right_wildcard] != false
|
173
|
+
res
|
142
174
|
end
|
143
175
|
|
144
176
|
def matches(value)
|
@@ -156,7 +188,7 @@ module SearchCopGrammar
|
|
156
188
|
|
157
189
|
class Float < WithoutMatches
|
158
190
|
def compatible?(value)
|
159
|
-
return true if value.to_s =~
|
191
|
+
return true if value.to_s =~ /^-?[0-9]+(\.[0-9]+)?$/
|
160
192
|
|
161
193
|
false
|
162
194
|
end
|
@@ -176,20 +208,26 @@ module SearchCopGrammar
|
|
176
208
|
|
177
209
|
class Datetime < WithoutMatches
|
178
210
|
def parse(value)
|
179
|
-
return value
|
180
|
-
|
181
|
-
if value =~ /^[0-9]{
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
elsif value =~ /^
|
186
|
-
::Time.new(
|
187
|
-
elsif value
|
211
|
+
return value..value unless value.is_a?(::String)
|
212
|
+
|
213
|
+
if value =~ /^[0-9]+ (hour|day|week|month|year)s{0,1} (ago)$/
|
214
|
+
number, period, ago = value.split(" ")
|
215
|
+
time = number.to_i.send(period.to_sym).send(ago.to_sym)
|
216
|
+
time..::Time.now
|
217
|
+
elsif value =~ /^[0-9]{4}$/
|
218
|
+
::Time.new(value).beginning_of_year..::Time.new(value).end_of_year
|
219
|
+
elsif value =~ %r{^([0-9]{4})(\.|-|/)([0-9]{1,2})$}
|
220
|
+
::Time.new(Regexp.last_match(1), Regexp.last_match(3), 15).beginning_of_month..::Time.new(Regexp.last_match(1), Regexp.last_match(3), 15).end_of_month
|
221
|
+
elsif value =~ %r{^([0-9]{1,2})(\.|-|/)([0-9]{4})$}
|
222
|
+
::Time.new(Regexp.last_match(3), Regexp.last_match(1), 15).beginning_of_month..::Time.new(Regexp.last_match(3), Regexp.last_match(1), 15).end_of_month
|
223
|
+
elsif value =~ %r{^[0-9]{4}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{1,2}$} || value =~ %r{^[0-9]{1,2}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{4}$}
|
188
224
|
time = ::Time.parse(value)
|
189
|
-
time.beginning_of_day
|
190
|
-
|
225
|
+
time.beginning_of_day..time.end_of_day
|
226
|
+
elsif value =~ %r{[0-9]{4}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{1,2}} || value =~ %r{[0-9]{1,2}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{4}}
|
191
227
|
time = ::Time.parse(value)
|
192
|
-
time
|
228
|
+
time..time
|
229
|
+
else
|
230
|
+
raise ArgumentError
|
193
231
|
end
|
194
232
|
rescue ArgumentError
|
195
233
|
raise SearchCop::IncompatibleDatatype, "Incompatible datatype for #{value}"
|
@@ -220,17 +258,23 @@ module SearchCopGrammar
|
|
220
258
|
|
221
259
|
class Date < Datetime
|
222
260
|
def parse(value)
|
223
|
-
return value
|
224
|
-
|
225
|
-
if value =~ /^[0-9]{
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
elsif value =~ /^
|
230
|
-
::Date.new(
|
231
|
-
|
261
|
+
return value..value unless value.is_a?(::String)
|
262
|
+
|
263
|
+
if value =~ /^[0-9]+ (day|week|month|year)s{0,1} (ago)$/
|
264
|
+
number, period, ago = value.split(" ")
|
265
|
+
time = number.to_i.send(period.to_sym).send(ago.to_sym)
|
266
|
+
time.to_date..::Date.today
|
267
|
+
elsif value =~ /^[0-9]{4}$/
|
268
|
+
::Date.new(value.to_i).beginning_of_year..::Date.new(value.to_i).end_of_year
|
269
|
+
elsif value =~ %r{^([0-9]{4})(\.|-|/)([0-9]{1,2})$}
|
270
|
+
::Date.new(Regexp.last_match(1).to_i, Regexp.last_match(3).to_i, 15).beginning_of_month..::Date.new(Regexp.last_match(1).to_i, Regexp.last_match(3).to_i, 15).end_of_month
|
271
|
+
elsif value =~ %r{^([0-9]{1,2})(\.|-|/)([0-9]{4})$}
|
272
|
+
::Date.new(Regexp.last_match(3).to_i, Regexp.last_match(1).to_i, 15).beginning_of_month..::Date.new(Regexp.last_match(3).to_i, Regexp.last_match(1).to_i, 15).end_of_month
|
273
|
+
elsif value =~ %r{[0-9]{4}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{1,2}} || value =~ %r{[0-9]{1,2}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{4}}
|
232
274
|
date = ::Date.parse(value)
|
233
|
-
date
|
275
|
+
date..date
|
276
|
+
else
|
277
|
+
raise ArgumentError
|
234
278
|
end
|
235
279
|
rescue ArgumentError
|
236
280
|
raise SearchCop::IncompatibleDatatype, "Incompatible datatype for #{value}"
|
@@ -249,4 +293,3 @@ module SearchCopGrammar
|
|
249
293
|
end
|
250
294
|
end
|
251
295
|
end
|
252
|
-
|
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
require "treetop"
|
3
2
|
|
4
3
|
module SearchCopGrammar
|
@@ -73,6 +72,7 @@ module SearchCopGrammar
|
|
73
72
|
class LessThan < Binary; end
|
74
73
|
class LessThanOrEqual < Binary; end
|
75
74
|
class Matches < Binary; end
|
75
|
+
class Generator < Binary; end
|
76
76
|
|
77
77
|
class Not
|
78
78
|
include Base
|
@@ -82,6 +82,12 @@ module SearchCopGrammar
|
|
82
82
|
def initialize(object)
|
83
83
|
@object = object
|
84
84
|
end
|
85
|
+
|
86
|
+
def finalize!
|
87
|
+
@object.finalize!
|
88
|
+
|
89
|
+
self
|
90
|
+
end
|
85
91
|
end
|
86
92
|
|
87
93
|
class MatchesFulltext < Binary
|
@@ -180,4 +186,3 @@ module SearchCopGrammar
|
|
180
186
|
end
|
181
187
|
end
|
182
188
|
end
|
183
|
-
|
data/search_cop.gemspec
CHANGED
@@ -1,29 +1,28 @@
|
|
1
|
-
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
1
|
+
lib = File.expand_path("lib", __dir__)
|
3
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
-
require
|
3
|
+
require "search_cop/version"
|
5
4
|
|
6
5
|
Gem::Specification.new do |spec|
|
7
6
|
spec.name = "search_cop"
|
8
7
|
spec.version = SearchCop::VERSION
|
9
8
|
spec.authors = ["Benjamin Vetter"]
|
10
9
|
spec.email = ["vetter@flakks.com"]
|
11
|
-
spec.description =
|
12
|
-
spec.summary =
|
10
|
+
spec.description = "Search engine like fulltext query support for ActiveRecord"
|
11
|
+
spec.summary = "Easily perform complex search engine like fulltext queries on your ActiveRecord models"
|
13
12
|
spec.homepage = "https://github.com/mrkamel/search_cop"
|
14
13
|
spec.license = "MIT"
|
15
14
|
|
16
|
-
spec.files = `git ls-files`.split(
|
15
|
+
spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
17
16
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
18
|
spec.require_paths = ["lib"]
|
20
19
|
|
21
20
|
spec.add_dependency "treetop"
|
22
21
|
|
23
|
-
spec.add_development_dependency "bundler", "~> 1.3"
|
24
|
-
spec.add_development_dependency "rake"
|
25
22
|
spec.add_development_dependency "activerecord", ">= 3.0.0"
|
26
|
-
spec.add_development_dependency "
|
27
|
-
spec.add_development_dependency "
|
23
|
+
spec.add_development_dependency "bundler"
|
24
|
+
spec.add_development_dependency "factory_bot"
|
28
25
|
spec.add_development_dependency "minitest"
|
26
|
+
spec.add_development_dependency "rake"
|
27
|
+
spec.add_development_dependency "rubocop"
|
29
28
|
end
|
data/test/and_test.rb
CHANGED
@@ -1,10 +1,9 @@
|
|
1
|
-
|
2
|
-
require File.expand_path("../test_helper", __FILE__)
|
1
|
+
require File.expand_path("test_helper", __dir__)
|
3
2
|
|
4
3
|
class AndTest < SearchCop::TestCase
|
5
4
|
def test_and_string
|
6
|
-
expected = create(:product, :
|
7
|
-
rejected = create(:product, :
|
5
|
+
expected = create(:product, title: "expected title", description: "Description")
|
6
|
+
rejected = create(:product, title: "Rejected title", description: "Description")
|
8
7
|
|
9
8
|
results = Product.search("title: 'Expected title' description: Description")
|
10
9
|
|
@@ -15,13 +14,12 @@ class AndTest < SearchCop::TestCase
|
|
15
14
|
end
|
16
15
|
|
17
16
|
def test_and_hash
|
18
|
-
expected = create(:product, :
|
19
|
-
rejected = create(:product, :
|
17
|
+
expected = create(:product, title: "Expected title", description: "Description")
|
18
|
+
rejected = create(:product, title: "Rejected title", description: "Description")
|
20
19
|
|
21
|
-
results = Product.search(:
|
20
|
+
results = Product.search(and: [{ title: "Expected title" }, { description: "Description" }])
|
22
21
|
|
23
22
|
assert_includes results, expected
|
24
23
|
refute_includes results, rejected
|
25
24
|
end
|
26
25
|
end
|
27
|
-
|
data/test/boolean_test.rb
CHANGED
@@ -1,15 +1,14 @@
|
|
1
|
-
|
2
|
-
require File.expand_path("../test_helper", __FILE__)
|
1
|
+
require File.expand_path("test_helper", __dir__)
|
3
2
|
|
4
3
|
class BooleanTest < SearchCop::TestCase
|
5
4
|
def test_mapping
|
6
|
-
product = create(:product, :
|
5
|
+
product = create(:product, available: true)
|
7
6
|
|
8
7
|
assert_includes Product.search("available: 1"), product
|
9
8
|
assert_includes Product.search("available: true"), product
|
10
9
|
assert_includes Product.search("available: yes"), product
|
11
10
|
|
12
|
-
product = create(:product, :
|
11
|
+
product = create(:product, available: false)
|
13
12
|
|
14
13
|
assert_includes Product.search("available: 0"), product
|
15
14
|
assert_includes Product.search("available: false"), product
|
@@ -17,28 +16,28 @@ class BooleanTest < SearchCop::TestCase
|
|
17
16
|
end
|
18
17
|
|
19
18
|
def test_anywhere
|
20
|
-
product = create(:product, :
|
19
|
+
product = create(:product, available: true)
|
21
20
|
|
22
21
|
assert_includes Product.search("true"), product
|
23
22
|
refute_includes Product.search("false"), product
|
24
23
|
end
|
25
24
|
|
26
25
|
def test_includes
|
27
|
-
product = create(:product, :
|
26
|
+
product = create(:product, available: true)
|
28
27
|
|
29
28
|
assert_includes Product.search("available: true"), product
|
30
29
|
refute_includes Product.search("available: false"), product
|
31
30
|
end
|
32
31
|
|
33
32
|
def test_equals
|
34
|
-
product = create(:product, :
|
33
|
+
product = create(:product, available: true)
|
35
34
|
|
36
35
|
assert_includes Product.search("available = true"), product
|
37
36
|
refute_includes Product.search("available = false"), product
|
38
37
|
end
|
39
38
|
|
40
39
|
def test_equals_not
|
41
|
-
product = create(:product, :
|
40
|
+
product = create(:product, available: false)
|
42
41
|
|
43
42
|
assert_includes Product.search("available != true"), product
|
44
43
|
refute_includes Product.search("available != false"), product
|
@@ -50,4 +49,3 @@ class BooleanTest < SearchCop::TestCase
|
|
50
49
|
end
|
51
50
|
end
|
52
51
|
end
|
53
|
-
|