Skip to content

Commit 723d025

Browse files
authored
Migrate from separate RBS files to inline annotations (#83)
* refactor: migrate from separate RBS files to inline annotations - Add rbs-inline annotations to source files - Remove separate sig/*.rbs files (now generated from source) - Update CI to generate RBS with rbs-inline before validation - Configure RuboCop to allow #: inline annotation syntax - Skip RBS generation for stdlib extensions (Array, Vector, Matrix) to avoid generic parameter conflicts Fixes #57 * refactor: add proper parameter types to RBS inline annotations Updated method signatures in lsi.rb and word_hash.rb to include proper parameter types instead of empty (). Methods now have full type signatures like `# @RBS (String, ?Float) -> ReturnType` instead of `# @RBS () -> ReturnType`. This completes the migration to inline RBS annotations with proper types for all method parameters. * feat: add Steep static type checking - Add steep gem for static type analysis - Create Steepfile configuration (ignoring stdlib extension files and complex LSI dual-mode code for now) - Add vendor RBS stubs for fast-stemmer, GSL, and Matrix dependencies - Fix type issues in Bayes#classify (handle nil from min_by) - Fix respond_to_missing? to return proper boolean - Add steep check to CI workflow * style: use single quotes in Steepfile * fix: skip steep on Ruby 4.0 (ffi incompatible) * chore: use Ruby 3.4 as default for lint job * fix: use install_if for steep to avoid lockfile conflict * fix: raise explicit error when no classifications available * fix: remove duplicate @categories RBS annotation
1 parent 870fef3 commit 723d025

24 files changed

Lines changed: 271 additions & 193 deletions

.github/workflows/ruby.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
- name: Set up Ruby
2626
uses: ruby/setup-ruby@v1
2727
with:
28-
ruby-version: '3.3'
28+
ruby-version: '3.4'
2929
bundler-cache: true
3030
- name: Run RuboCop
3131
run: bundle exec rubocop
@@ -45,7 +45,10 @@ jobs:
4545
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
4646
- name: Run tests
4747
run: bundle exec rake test
48-
- name: Install RBS
49-
run: gem install rbs
48+
- name: Generate RBS from inline annotations
49+
run: bundle exec rbs-inline --output sig lib/
5050
- name: Validate RBS types
51-
run: rbs -I sig validate
51+
run: bundle exec rbs -I sig validate
52+
- name: Type check with Steep
53+
if: matrix.ruby-version != '4.0'
54+
run: bundle exec steep check

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ pkg/
1616

1717
# Claude Code local settings
1818
.claude/
19+
sig/generated/

.rubocop.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
plugins:
22
- rubocop-minitest
33

4+
# Allow rbs-inline #: comments without space after #
5+
Layout/LeadingCommentSpace:
6+
AllowRBSInlineAnnotation: true
7+
48
AllCops:
59
TargetRubyVersion: 3.1
610
NewCops: enable

Gemfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,8 @@ end
1313
group :development do
1414
gem 'rubocop', require: false
1515
gem 'rubocop-minitest', require: false
16+
# steep's ffi dependency doesn't support Ruby 4.0 yet
17+
install_if -> { RUBY_VERSION < '4.0' } do
18+
gem 'steep', require: false
19+
end
1620
end

Gemfile.lock

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,44 @@ PATH
1010
GEM
1111
remote: https://rubygems.org/
1212
specs:
13+
activesupport (8.1.1)
14+
base64
15+
bigdecimal
16+
concurrent-ruby (~> 1.0, >= 1.3.1)
17+
connection_pool (>= 2.2.5)
18+
drb
19+
i18n (>= 1.6, < 2)
20+
json
21+
logger (>= 1.4.2)
22+
minitest (>= 5.1)
23+
securerandom (>= 0.3)
24+
tzinfo (~> 2.0, >= 2.0.5)
25+
uri (>= 0.13.1)
1326
ast (2.4.3)
27+
base64 (0.3.0)
28+
bigdecimal (4.0.1)
29+
concurrent-ruby (1.3.6)
30+
connection_pool (3.0.2)
31+
csv (3.3.5)
1432
docile (1.4.1)
33+
drb (2.2.3)
1534
fast-stemmer (1.0.2)
35+
ffi (1.17.2-arm64-darwin)
36+
ffi (1.17.2-x86_64-linux-gnu)
37+
fileutils (1.8.0)
38+
i18n (1.14.8)
39+
concurrent-ruby (~> 1.0)
1640
json (2.18.0)
1741
language_server-protocol (3.17.0.5)
1842
lint_roller (1.1.0)
43+
listen (3.9.0)
44+
rb-fsevent (~> 0.10, >= 0.10.3)
45+
rb-inotify (~> 0.9, >= 0.9.10)
46+
logger (1.7.0)
1947
matrix (0.4.2)
2048
minitest (6.0.1)
2149
prism (~> 1.5)
22-
mutex_m (0.2.0)
50+
mutex_m (0.3.0)
2351
ostruct (0.6.3)
2452
parallel (1.27.0)
2553
parser (3.3.10.0)
@@ -31,6 +59,14 @@ GEM
3159
racc (1.8.1)
3260
rainbow (3.1.1)
3361
rake (13.3.1)
62+
rb-fsevent (0.11.2)
63+
rb-inotify (0.11.1)
64+
ffi (~> 1.0)
65+
rbs (3.10.0)
66+
logger
67+
rbs-inline (0.12.0)
68+
prism (>= 0.29)
69+
rbs (>= 3.8.0)
3470
rdoc (6.5.1.1)
3571
psych (>= 4.0.0)
3672
regexp_parser (2.11.3)
@@ -53,16 +89,40 @@ GEM
5389
rubocop (>= 1.75.0, < 2.0)
5490
rubocop-ast (>= 1.38.0, < 2.0)
5591
ruby-progressbar (1.13.0)
92+
securerandom (0.4.1)
5693
simplecov (0.22.0)
5794
docile (~> 1.1)
5895
simplecov-html (~> 0.11)
5996
simplecov_json_formatter (~> 0.1)
6097
simplecov-html (0.13.2)
6198
simplecov_json_formatter (0.1.4)
99+
steep (1.10.0)
100+
activesupport (>= 5.1)
101+
concurrent-ruby (>= 1.1.10)
102+
csv (>= 3.0.9)
103+
fileutils (>= 1.1.0)
104+
json (>= 2.1.0)
105+
language_server-protocol (>= 3.17.0.4, < 4.0)
106+
listen (~> 3.0)
107+
logger (>= 1.3.0)
108+
mutex_m (>= 0.3.0)
109+
parser (>= 3.1)
110+
rainbow (>= 2.2.2, < 4.0)
111+
rbs (~> 3.9)
112+
securerandom (>= 0.1)
113+
strscan (>= 1.0.0)
114+
terminal-table (>= 2, < 5)
115+
uri (>= 0.12.0)
62116
stringio (3.1.0)
117+
strscan (3.1.6)
118+
terminal-table (4.0.0)
119+
unicode-display_width (>= 1.1.1, < 4)
120+
tzinfo (2.0.6)
121+
concurrent-ruby (~> 1.0)
63122
unicode-display_width (3.2.0)
64123
unicode-emoji (~> 4.1)
65124
unicode-emoji (4.2.0)
125+
uri (1.1.1)
66126

67127
PLATFORMS
68128
arm64-darwin-22
@@ -77,10 +137,12 @@ DEPENDENCIES
77137
minitest
78138
mutex_m
79139
ostruct
140+
rbs-inline
80141
rdoc
81142
rubocop
82143
rubocop-minitest
83144
simplecov
145+
steep
84146

85147
BUNDLED WITH
86148
2.4.17

Steepfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
D = Steep::Diagnostic
2+
3+
target :lib do
4+
signature 'sig'
5+
6+
check 'lib'
7+
8+
# Ignore files that patch stdlib classes (these cause conflicts)
9+
ignore 'lib/classifier/extensions/vector.rb'
10+
ignore 'lib/classifier/extensions/vector_serialize.rb'
11+
ignore 'lib/classifier/extensions/string.rb'
12+
ignore 'lib/classifier/extensions/word_hash.rb'
13+
14+
# Ignore LSI files for now due to complex GSL/Matrix dual-mode typing
15+
ignore 'lib/classifier/lsi.rb'
16+
ignore 'lib/classifier/lsi/content_node.rb'
17+
ignore 'lib/classifier/lsi/summary.rb'
18+
end

classifier.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ Gem::Specification.new do |s|
1414
s.add_dependency 'rake'
1515
s.add_dependency 'matrix'
1616
s.add_development_dependency 'minitest'
17+
s.add_development_dependency 'rbs-inline'
1718
s.add_development_dependency 'rdoc'
1819
end

lib/classifier/bayes.rb

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1+
# rbs_inline: enabled
2+
13
# Author:: Lucas Carlson (mailto:lucas@rufy.com)
24
# Copyright:: Copyright (c) 2005 Lucas Carlson
35
# License:: LGPL
46

57
module Classifier
68
class Bayes
9+
# @rbs @categories: Hash[Symbol, Hash[Symbol, Integer]]
10+
# @rbs @total_words: Integer
11+
# @rbs @category_counts: Hash[Symbol, Integer]
12+
# @rbs @category_word_count: Hash[Symbol, Integer]
13+
714
# The class can be created with one or more categories, each of which will be
815
# initialized and given a training method. E.g.,
916
# b = Classifier::Bayes.new 'Interesting', 'Uninteresting', 'Spam'
17+
# @rbs (*String | Symbol) -> void
1018
def initialize(*categories)
1119
@categories = {}
1220
categories.each { |category| @categories[category.prepare_category_name] = {} }
@@ -15,13 +23,14 @@ def initialize(*categories)
1523
@category_word_count = Hash.new(0)
1624
end
1725

18-
#
1926
# Provides a general training method for all categories specified in Bayes#new
2027
# For example:
2128
# b = Classifier::Bayes.new 'This', 'That', 'the_other'
2229
# b.train :this, "This text"
2330
# b.train "that", "That text"
2431
# b.train "The other", "The other text"
32+
#
33+
# @rbs (String | Symbol, String) -> void
2534
def train(category, text)
2635
category = category.prepare_category_name
2736
@category_counts[category] += 1
@@ -33,14 +42,15 @@ def train(category, text)
3342
end
3443
end
3544

36-
#
3745
# Provides a untraining method for all categories specified in Bayes#new
3846
# Be very careful with this method.
3947
#
4048
# For example:
4149
# b = Classifier::Bayes.new 'This', 'That', 'the_other'
4250
# b.train :this, "This text"
4351
# b.untrain :this, "This text"
52+
#
53+
# @rbs (String | Symbol, String) -> void
4454
def untrain(category, text)
4555
category = category.prepare_category_name
4656
@category_counts[category] -= 1
@@ -59,11 +69,12 @@ def untrain(category, text)
5969
end
6070
end
6171

62-
#
6372
# Returns the scores in each category the provided +text+. E.g.,
6473
# b.classifications "I hate bad words and you"
6574
# => {"Uninteresting"=>-12.6997928013932, "Interesting"=>-18.4206807439524}
6675
# The largest of these scores (the one closest to 0) is the one picked out by #classify
76+
#
77+
# @rbs (String) -> Hash[String, Float]
6778
def classifications(text)
6879
words = text.word_hash.keys
6980
training_count = @category_counts.values.sum.to_f
@@ -80,13 +91,17 @@ def classifications(text)
8091
end
8192
end
8293

83-
#
8494
# Returns the classification of the provided +text+, which is one of the
8595
# categories given in the initializer. E.g.,
8696
# b.classify "I hate bad words and you"
8797
# => 'Uninteresting'
98+
#
99+
# @rbs (String) -> String
88100
def classify(text)
89-
classifications(text).min_by { |a| -a[1] }[0]
101+
best = classifications(text).min_by { |a| -a[1] }
102+
raise StandardError, 'No classifications available' unless best
103+
104+
best.first.to_s
90105
end
91106

92107
#
@@ -107,20 +122,21 @@ def method_missing(name, *args)
107122
args.each { |text| send(method, category, text) }
108123
end
109124

125+
# @rbs (Symbol, ?bool) -> bool
110126
def respond_to_missing?(name, include_private = false)
111-
name.to_s =~ /(un)?train_(\w+)/ || super
127+
!!(name.to_s =~ /(un)?train_(\w+)/) || super
112128
end
113129

114-
#
115130
# Provides a list of category names
116131
# For example:
117132
# b.categories
118133
# => ['This', 'That', 'the_other']
119-
def categories # :nodoc:
134+
#
135+
# @rbs () -> Array[String]
136+
def categories
120137
@categories.keys.collect(&:to_s)
121138
end
122139

123-
#
124140
# Allows you to add categories to the classifier.
125141
# For example:
126142
# b.add_category "Not spam"
@@ -129,20 +145,23 @@ def categories # :nodoc:
129145
# result in an undertrained category that will tend to match
130146
# more criteria than the trained selective categories. In short,
131147
# try to initialize your categories at initialization.
148+
#
149+
# @rbs (String | Symbol) -> Hash[Symbol, Integer]
132150
def add_category(category)
133151
@categories[category.prepare_category_name] = {}
134152
end
135153

136154
alias append_category add_category
137155

138-
#
139156
# Allows you to remove categories from the classifier.
140157
# For example:
141158
# b.remove_category "Spam"
142159
#
143160
# WARNING: Removing categories from a trained classifier will
144161
# result in the loss of all training data for that category.
145162
# Make sure you really want to do this before calling this method.
163+
#
164+
# @rbs (String | Symbol) -> void
146165
def remove_category(category)
147166
category = category.prepare_category_name
148167
raise StandardError, "No such category: #{category}" unless @categories.key?(category)

lib/classifier/extensions/vector.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
# rbs_inline: enabled
2+
13
# Author:: Ernest Ellingson
24
# Copyright:: Copyright (c) 2005
35

46
# These are extensions to the std-lib 'matrix' to allow an all ruby SVD
57

68
require 'matrix'
79

10+
# @rbs skip
811
class Array
912
def sum_with_identity(identity = 0.0, &)
1013
return identity unless size.to_i.positive?
@@ -14,6 +17,7 @@ def sum_with_identity(identity = 0.0, &)
1417
end
1518
end
1619

20+
# @rbs skip
1721
class Vector
1822
EPSILON = 1e-10
1923

@@ -37,6 +41,7 @@ def normalize
3741
end
3842
end
3943

44+
# @rbs skip
4045
class Matrix
4146
def self.diag(diagonal_elements)
4247
Matrix.diagonal(*diagonal_elements)

0 commit comments

Comments
 (0)