Skip to content

Commit 2f9e2ab

Browse files
committed
chore: add RuboCop for code standardization
Add RuboCop with rubocop-minitest for consistent code style: - Configure .rubocop.yml with project-specific rules - Exclude legacy bin/ and install.rb files - Allow complexity in mathematical algorithms (SVD, LSI) - Auto-correct 151 style offenses - Add respond_to_missing? to Bayes class - Add lint job to CI workflow Style improvements applied: - Consistent single quotes for strings - Proper spacing and indentation - Modern Ruby idioms (anonymous block forwarding) - Minitest assertion best practices Fixes #56
1 parent 8bff7c5 commit 2f9e2ab

12 files changed

Lines changed: 270 additions & 62 deletions

File tree

.github/workflows/ruby.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,19 @@ permissions:
1717
contents: read
1818

1919
jobs:
20-
test:
20+
lint:
21+
runs-on: ubuntu-latest
22+
steps:
23+
- uses: actions/checkout@v4
24+
- name: Set up Ruby
25+
uses: ruby/setup-ruby@v1
26+
with:
27+
ruby-version: '3.3'
28+
bundler-cache: true
29+
- name: Run RuboCop
30+
run: bundle exec rubocop
2131

32+
test:
2233
runs-on: ubuntu-latest
2334
strategy:
2435
matrix:

.rubocop.yml

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
plugins:
2+
- rubocop-minitest
3+
4+
AllCops:
5+
TargetRubyVersion: 3.1
6+
NewCops: enable
7+
Exclude:
8+
- 'vendor/**/*'
9+
- 'coverage/**/*'
10+
- '*.gemspec'
11+
- 'bin/**/*'
12+
- 'install.rb'
13+
14+
# Existing code lacks documentation, can add incrementally
15+
Style/Documentation:
16+
Enabled: false
17+
18+
# Test files often have long blocks
19+
Metrics/BlockLength:
20+
Exclude:
21+
- 'test/**/*'
22+
- 'lib/classifier/extensions/vector.rb'
23+
24+
# Allow longer methods in complex algorithms (SVD, etc.)
25+
Metrics/MethodLength:
26+
Max: 25
27+
Exclude:
28+
- 'test/**/*'
29+
- 'lib/classifier/extensions/vector.rb'
30+
- 'lib/classifier/lsi/content_node.rb'
31+
32+
# Allow higher complexity for mathematical algorithms
33+
Metrics/AbcSize:
34+
Max: 30
35+
Exclude:
36+
- 'test/**/*'
37+
- 'lib/classifier/extensions/vector.rb'
38+
- 'lib/classifier/lsi.rb'
39+
- 'lib/classifier/lsi/content_node.rb'
40+
41+
Metrics/CyclomaticComplexity:
42+
Max: 10
43+
Exclude:
44+
- 'lib/classifier/extensions/vector.rb'
45+
- 'lib/classifier/lsi/content_node.rb'
46+
47+
Metrics/PerceivedComplexity:
48+
Max: 10
49+
Exclude:
50+
- 'lib/classifier/extensions/vector.rb'
51+
- 'lib/classifier/lsi/content_node.rb'
52+
53+
# Class length limits - algorithms and tests can be longer
54+
Metrics/ClassLength:
55+
Max: 250
56+
Exclude:
57+
- 'test/**/*'
58+
59+
# SV_decomp is a standard algorithm name
60+
Naming/MethodName:
61+
Exclude:
62+
- 'lib/classifier/extensions/vector.rb'
63+
64+
# Short parameter names are acceptable for serialization
65+
Naming/MethodParameterName:
66+
Exclude:
67+
- 'lib/classifier/extensions/vector_serialize.rb'
68+
69+
# Marshal.load is intentional for deserialization
70+
Security/MarshalLoad:
71+
Exclude:
72+
- 'lib/classifier/extensions/vector_serialize.rb'
73+
- 'test/**/*'
74+
75+
# CORPUS_SKIP_WORDS is a public constant used externally
76+
Lint/UselessConstantScoping:
77+
Enabled: false
78+
79+
# Frozen string literal is optional for this gem
80+
Style/FrozenStringLiteralComment:
81+
Enabled: false
82+
83+
# Allow compact class/module child definitions
84+
Style/ClassAndModuleChildren:
85+
Enabled: false
86+
87+
# Allow both styles of string literals
88+
Style/StringLiterals:
89+
EnforcedStyle: single_quotes
90+
91+
Style/StringLiteralsInInterpolation:
92+
EnforcedStyle: single_quotes
93+
94+
# Minitest assertions
95+
Minitest/MultipleAssertions:
96+
Max: 10

Gemfile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@ gem 'mutex_m'
88
group :test do
99
gem 'simplecov', require: false
1010
end
11+
12+
group :development do
13+
gem 'rubocop', require: false
14+
gem 'rubocop-minitest', require: false
15+
end

Gemfile.lock

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,57 @@ PATH
1010
GEM
1111
remote: https://rubygems.org/
1212
specs:
13+
ast (2.4.3)
1314
docile (1.4.1)
1415
fast-stemmer (1.0.2)
16+
json (2.18.0)
17+
language_server-protocol (3.17.0.5)
18+
lint_roller (1.1.0)
1519
matrix (0.4.2)
1620
minitest (5.18.1)
1721
mutex_m (0.2.0)
22+
parallel (1.27.0)
23+
parser (3.3.10.0)
24+
ast (~> 2.4.1)
25+
racc
26+
prism (1.7.0)
1827
psych (5.1.2)
1928
stringio
29+
racc (1.8.1)
30+
rainbow (3.1.1)
2031
rake (13.0.6)
2132
rdoc (6.5.1.1)
2233
psych (>= 4.0.0)
34+
regexp_parser (2.11.3)
35+
rubocop (1.82.1)
36+
json (~> 2.3)
37+
language_server-protocol (~> 3.17.0.2)
38+
lint_roller (~> 1.1.0)
39+
parallel (~> 1.10)
40+
parser (>= 3.3.0.2)
41+
rainbow (>= 2.2.2, < 4.0)
42+
regexp_parser (>= 2.9.3, < 3.0)
43+
rubocop-ast (>= 1.48.0, < 2.0)
44+
ruby-progressbar (~> 1.7)
45+
unicode-display_width (>= 2.4.0, < 4.0)
46+
rubocop-ast (1.48.0)
47+
parser (>= 3.3.7.2)
48+
prism (~> 1.4)
49+
rubocop-minitest (0.38.2)
50+
lint_roller (~> 1.1)
51+
rubocop (>= 1.75.0, < 2.0)
52+
rubocop-ast (>= 1.38.0, < 2.0)
53+
ruby-progressbar (1.13.0)
2354
simplecov (0.22.0)
2455
docile (~> 1.1)
2556
simplecov-html (~> 0.11)
2657
simplecov_json_formatter (~> 0.1)
2758
simplecov-html (0.13.2)
2859
simplecov_json_formatter (0.1.4)
2960
stringio (3.1.0)
61+
unicode-display_width (3.2.0)
62+
unicode-emoji (~> 4.1)
63+
unicode-emoji (4.2.0)
3064

3165
PLATFORMS
3266
arm64-darwin-22
@@ -41,6 +75,8 @@ DEPENDENCIES
4175
minitest
4276
mutex_m
4377
rdoc
78+
rubocop
79+
rubocop-minitest
4480
simplecov
4581

4682
BUNDLED WITH

lib/classifier/bayes.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def classifications(text)
8888
# b.classify "I hate bad words and you"
8989
# => 'Uninteresting'
9090
def classify(text)
91-
(classifications(text).sort_by { |a| -a[1] })[0][0]
91+
classifications(text).min_by { |a| -a[1] }[0]
9292
end
9393

9494
#
@@ -109,6 +109,10 @@ def method_missing(name, *args)
109109
args.each { |text| send(method, category, text) }
110110
end
111111

112+
def respond_to_missing?(name, include_private = false)
113+
name.to_s =~ /(un)?train_(\w+)/ || super
114+
end
115+
112116
#
113117
# Provides a list of category names
114118
# For example:

lib/classifier/extensions/vector.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
require 'matrix'
77

88
class Array
9-
def sum_with_identity(identity = 0.0, &block)
9+
def sum_with_identity(identity = 0.0, &)
1010
return identity unless size.to_i.positive?
11-
return map(&block).sum_with_identity(identity) if block_given?
11+
return map(&).sum_with_identity(identity) if block_given?
1212

13-
compact.reduce(:+).to_f || identity.to_f
13+
compact.reduce(:+).to_f
1414
end
1515
end
1616

@@ -58,8 +58,8 @@ def SV_decomp(max_sweeps = 20)
5858

5959
loop do
6060
iteration_count += 1
61-
(0...q_rotation_matrix.row_size - 1).each do |row|
62-
(1..q_rotation_matrix.row_size - 1).each do |col|
61+
(0...(q_rotation_matrix.row_size - 1)).each do |row|
62+
(1..(q_rotation_matrix.row_size - 1)).each do |col|
6363
next if row == col
6464

6565
angle = Math.atan((2.to_r * q_rotation_matrix[row,

lib/classifier/extensions/word_hash.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class String
1212
# "Hello (greeting's), with {braces} < >...?".without_punctuation
1313
# => "Hello greetings with braces "
1414
def without_punctuation
15-
tr(',?.!;:"@#$%^&*()_=+[]{}\|<>/`~', ' ').tr("'\-", '')
15+
tr(',?.!;:"@#$%^&*()_=+[]{}|<>/`~', ' ').tr("'-", '')
1616
end
1717

1818
# Return a Hash of strings => ints. Each word in the string is stemmed,

lib/classifier/lsi.rb

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ def highest_relative_content(max_chunks = 10)
165165
avg_density = {}
166166
@items.each_key { |x| avg_density[x] = proximity_array_for_content(x).inject(0.0) { |i, j| i + j[1] } }
167167

168-
avg_density.keys.sort_by { |x| avg_density[x] }.reverse[0..max_chunks - 1].map
168+
avg_density.keys.sort_by { |x| avg_density[x] }.reverse[0..(max_chunks - 1)].map
169169
end
170170

171171
# This function is the primitive that find_related and classify
@@ -180,10 +180,10 @@ def highest_relative_content(max_chunks = 10)
180180
# The parameter doc is the content to compare. If that content is not
181181
# indexed, you can pass an optional block to define how to create the
182182
# text data. See add_item for examples of how this works.
183-
def proximity_array_for_content(doc, &block)
183+
def proximity_array_for_content(doc, &)
184184
return [] if needs_rebuild?
185185

186-
content_node = node_for_content(doc, &block)
186+
content_node = node_for_content(doc, &)
187187
result =
188188
@items.keys.collect do |item|
189189
val = if self.class.gsl_available
@@ -201,10 +201,10 @@ def proximity_array_for_content(doc, &block)
201201
# calculated vectors instead of their full versions. This is useful when
202202
# you're trying to perform operations on content that is much smaller than
203203
# the text you're working with. search uses this primitive.
204-
def proximity_norms_for_content(doc, &block)
204+
def proximity_norms_for_content(doc, &)
205205
return [] if needs_rebuild?
206206

207-
content_node = node_for_content(doc, &block)
207+
content_node = node_for_content(doc, &)
208208
result =
209209
@items.keys.collect do |item|
210210
val = if self.class.gsl_available
@@ -229,7 +229,7 @@ def search(string, max_nearest = 3)
229229

230230
carry = proximity_norms_for_content(string)
231231
result = carry.collect { |x| x[0] }
232-
result[0..max_nearest - 1]
232+
result[0..(max_nearest - 1)]
233233
end
234234

235235
# This function takes content and finds other documents
@@ -245,7 +245,7 @@ def find_related(doc, max_nearest = 3, &block)
245245
carry =
246246
proximity_array_for_content(doc, &block).reject { |pair| pair[0] == doc }
247247
result = carry.collect { |x| x[0] }
248-
result[0..max_nearest - 1]
248+
result[0..(max_nearest - 1)]
249249
end
250250

251251
# This function uses a voting system to categorize documents, based on
@@ -257,17 +257,17 @@ def find_related(doc, max_nearest = 3, &block)
257257
# text. A cutoff of 1 means that every document in the index votes on
258258
# what category the document is in. This may not always make sense.
259259
#
260-
def classify(doc, cutoff = 0.30, &block)
261-
votes = vote(doc, cutoff, &block)
260+
def classify(doc, cutoff = 0.30, &)
261+
votes = vote(doc, cutoff, &)
262262

263263
ranking = votes.keys.sort_by { |x| votes[x] }
264264
ranking[-1]
265265
end
266266

267-
def vote(doc, cutoff = 0.30, &block)
267+
def vote(doc, cutoff = 0.30, &)
268268
icutoff = (@items.size * cutoff).round
269-
carry = proximity_array_for_content(doc, &block)
270-
carry = carry[0..icutoff - 1]
269+
carry = proximity_array_for_content(doc, &)
270+
carry = carry[0..(icutoff - 1)]
271271
votes = {}
272272
carry.each do |pair|
273273
categories = @items[pair[0]].categories
@@ -291,8 +291,8 @@ def vote(doc, cutoff = 0.30, &block)
291291
#
292292
#
293293
# See classify() for argument docs
294-
def classify_with_confidence(doc, cutoff = 0.30, &block)
295-
votes = vote(doc, cutoff, &block)
294+
def classify_with_confidence(doc, cutoff = 0.30, &)
295+
votes = vote(doc, cutoff, &)
296296
votes_sum = votes.values.inject(0.0) { |sum, v| sum + v }
297297
return [nil, nil] if votes_sum.zero?
298298

@@ -309,7 +309,7 @@ def highest_ranked_stems(doc, count = 3)
309309
raise 'Requested stem ranking on non-indexed content!' unless @items[doc]
310310

311311
arr = node_for_content(doc).lsi_vector.to_a
312-
top_n = arr.sort.reverse[0..count - 1]
312+
top_n = arr.sort.reverse[0..(count - 1)]
313313
top_n.collect { |x| @word_list.word_for_index(arr.index(x)) }
314314
end
315315

0 commit comments

Comments
 (0)