From eec680c99f22830ed61d4eac3352be67172b02b2 Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Thu, 8 Jan 2026 11:34:57 -0500 Subject: [PATCH 1/6] Migrate `Color` enum to a bare Ruby class Signed-off-by: Alexandre Terrasa --- lib/spoom/colors.rb | 51 +++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/lib/spoom/colors.rb b/lib/spoom/colors.rb index 049a39da..590f082d 100644 --- a/lib/spoom/colors.rb +++ b/lib/spoom/colors.rb @@ -2,34 +2,35 @@ # frozen_string_literal: true module Spoom - class Color < T::Enum - enums do - CLEAR = new("\e[0m") - BOLD = new("\e[1m") + class Color + #: String + attr_reader :ansi_code - BLACK = new("\e[30m") - RED = new("\e[31m") - GREEN = new("\e[32m") - YELLOW = new("\e[33m") - BLUE = new("\e[34m") - MAGENTA = new("\e[35m") - CYAN = new("\e[36m") - WHITE = new("\e[37m") - - LIGHT_BLACK = new("\e[90m") - LIGHT_RED = new("\e[91m") - LIGHT_GREEN = new("\e[92m") - LIGHT_YELLOW = new("\e[93m") - LIGHT_BLUE = new("\e[94m") - LIGHT_MAGENTA = new("\e[95m") - LIGHT_CYAN = new("\e[96m") - LIGHT_WHITE = new("\e[97m") + #: (String) -> void + def initialize(ansi_code) + @ansi_code = ansi_code end - #: -> String - def ansi_code - serialize - end + CLEAR = new("\e[0m") #: Color + BOLD = new("\e[1m") #: Color + + BLACK = new("\e[30m") #: Color + RED = new("\e[31m") #: Color + GREEN = new("\e[32m") #: Color + YELLOW = new("\e[33m") #: Color + BLUE = new("\e[34m") #: Color + MAGENTA = new("\e[35m") #: Color + CYAN = new("\e[36m") #: Color + WHITE = new("\e[37m") #: Color + + LIGHT_BLACK = new("\e[90m") #: Color + LIGHT_RED = new("\e[91m") #: Color + LIGHT_GREEN = new("\e[92m") #: Color + LIGHT_YELLOW = new("\e[93m") #: Color + LIGHT_BLUE = new("\e[94m") #: Color + LIGHT_MAGENTA = new("\e[95m") #: Color + LIGHT_CYAN = new("\e[96m") #: Color + LIGHT_WHITE = new("\e[97m") #: Color end module Colorize From b286e11d4496c245131dc1719a39c11c9dcb6f40 Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Thu, 8 Jan 2026 11:37:46 -0500 Subject: [PATCH 2/6] Migrate `Deadcode::Definition` enums to bare Ruby classes Signed-off-by: Alexandre Terrasa --- lib/spoom/cli/deadcode.rb | 4 ++-- lib/spoom/deadcode/definition.rb | 41 +++++++++++++++++++------------- lib/spoom/deadcode/remover.rb | 2 ++ 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/lib/spoom/cli/deadcode.rb b/lib/spoom/cli/deadcode.rb index 5276ac39..cb02572e 100644 --- a/lib/spoom/cli/deadcode.rb +++ b/lib/spoom/cli/deadcode.rb @@ -106,7 +106,7 @@ def deadcode(*paths) index.definitions.each do |name, definitions| $stderr.puts " #{blue(name)}" definitions.each do |definition| - $stderr.puts " #{yellow(definition.kind.serialize)} #{gray(definition.location.to_s)}" + $stderr.puts " #{yellow(definition.kind.to_s)} #{gray(definition.location.to_s)}" end end $stderr.puts @@ -116,7 +116,7 @@ def deadcode(*paths) $stderr.puts "\nReferences:" index.references.values.flatten.sort_by(&:name).each do |references| name = references.name - kind = references.kind.serialize + kind = references.kind.to_s loc = references.location.to_s $stderr.puts " #{blue(name)} #{yellow(kind)} #{gray(loc)}" end diff --git a/lib/spoom/deadcode/definition.rb b/lib/spoom/deadcode/definition.rb index 7b4d3226..965753e6 100644 --- a/lib/spoom/deadcode/definition.rb +++ b/lib/spoom/deadcode/definition.rb @@ -5,26 +5,33 @@ module Spoom module Deadcode # A definition is a class, module, method, constant, etc. being defined in the code class Definition < T::Struct - class Kind < T::Enum - enums do - AttrReader = new("attr_reader") - AttrWriter = new("attr_writer") - Class = new("class") - Constant = new("constant") - Method = new("method") - Module = new("module") + class Kind + #: (String) -> void + def initialize(name) + @name = name end - end - class Status < T::Enum - enums do - # A definition is marked as `ALIVE` if it has at least one reference with the same name - ALIVE = new - # A definition is marked as `DEAD` if it has no reference with the same name - DEAD = new - # A definition can be marked as `IGNORED` if it is not relevant for the analysis - IGNORED = new + # @override + #: -> String + def to_s + @name end + + AttrReader = new("attr_reader") #: Kind + AttrWriter = new("attr_writer") #: Kind + Class = new("class") #: Kind + Constant = new("constant") #: Kind + Method = new("method") #: Kind + Module = new("module") #: Kind + end + + class Status + # A definition is marked as `ALIVE` if it has at least one reference with the same name + ALIVE = new #: Status + # A definition is marked as `DEAD` if it has no reference with the same name + DEAD = new #: Status + # A definition can be marked as `IGNORED` if it is not relevant for the analysis + IGNORED = new #: Status end const :kind, Kind diff --git a/lib/spoom/deadcode/remover.rb b/lib/spoom/deadcode/remover.rb index 6d4ff6b0..b6df98fa 100644 --- a/lib/spoom/deadcode/remover.rb +++ b/lib/spoom/deadcode/remover.rb @@ -585,6 +585,8 @@ def node_match_kind?(node, kind) node.is_a?(Prism::DefNode) when Definition::Kind::Module node.is_a?(Prism::ModuleNode) + else + raise Error, "Unsupported node kind: #{node.class}" end end end From a57b2e98f86d3aef1160ec57963104cb2138d3cb Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Thu, 8 Jan 2026 11:38:03 -0500 Subject: [PATCH 3/6] Migrate `Model::Visibility` enum to a bare Ruby class Signed-off-by: Alexandre Terrasa --- lib/spoom/model/builder.rb | 2 +- lib/spoom/model/model.rb | 32 +++++++++++++++++++++++++++----- test/spoom/model/builder_test.rb | 4 ++-- 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/lib/spoom/model/builder.rb b/lib/spoom/model/builder.rb index 2b05e591..af91ff0e 100644 --- a/lib/spoom/model/builder.rb +++ b/lib/spoom/model/builder.rb @@ -226,7 +226,7 @@ def visit_call_node(node) current_namespace.mixins << Extend.new(arg.slice) end when :public, :private, :protected - @visibility_stack << Visibility.from_serialized(node.name.to_s) + @visibility_stack << Visibility.from_string(node.name.to_s) if node.arguments super @visibility_stack.pop diff --git a/lib/spoom/model/model.rb b/lib/spoom/model/model.rb index 9abefbba..e0ab7e1b 100644 --- a/lib/spoom/model/model.rb +++ b/lib/spoom/model/model.rb @@ -192,12 +192,34 @@ class AttrReader < Attr; end class AttrWriter < Attr; end class AttrAccessor < Attr; end - class Visibility < T::Enum - enums do - Public = new("public") - Protected = new("protected") - Private = new("private") + class Visibility + class << self + #: (String) -> Visibility + def from_string(name) + case name + when "public" then Public + when "protected" then Protected + when "private" then Private + else + raise Error, "Invalid visibility: #{name}" + end + end + end + + #: (String) -> void + def initialize(name) + @name = name end + + # @override + #: -> String + def to_s + @name + end + + Public = new("public") #: Visibility + Protected = new("protected") #: Visibility + Private = new("private") #: Visibility end # A mixin (include, prepend, extend) to a namespace diff --git a/test/spoom/model/builder_test.rb b/test/spoom/model/builder_test.rb index 6d9687f5..7bfaf5a9 100644 --- a/test/spoom/model/builder_test.rb +++ b/test/spoom/model/builder_test.rb @@ -353,7 +353,7 @@ def m7; end model.symbols.values .flat_map(&:definitions) .grep(Method) - .map { |d| "#{d.full_name}: #{T.cast(d, Method).visibility.serialize}" }, + .map { |d| "#{d.full_name}: #{d.visibility}" }, ) end @@ -390,7 +390,7 @@ def m6; end model.symbols.values .flat_map(&:definitions) .grep(Method) - .map { |d| "#{d.full_name}: #{T.cast(d, Method).visibility.serialize}" }, + .map { |d| "#{d.full_name}: #{d.visibility}" }, ) end From 62cd6bd132585546fcea3bec2566857f127c4c60 Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Thu, 8 Jan 2026 11:38:16 -0500 Subject: [PATCH 4/6] Migrate `Reference::Kind` enum to a bare Ruby class Signed-off-by: Alexandre Terrasa --- lib/spoom/model/reference.rb | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/spoom/model/reference.rb b/lib/spoom/model/reference.rb index 63130ce9..4908ded3 100644 --- a/lib/spoom/model/reference.rb +++ b/lib/spoom/model/reference.rb @@ -8,11 +8,19 @@ class Model # Constants could be classes, modules, or actual constants. # Methods could be accessors, instance or class methods, aliases, etc. class Reference < T::Struct - class Kind < T::Enum - enums do - Constant = new("constant") - Method = new("method") + class Kind + #: (String) -> void + def initialize(name) + @name = name end + + #: -> String + def to_s + @name + end + + Constant = new("constant") #: Kind + Method = new("method") #: Kind end class << self From dcab5c7e5688be8ec03ba15e88e944e6cf952463 Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Thu, 8 Jan 2026 11:42:58 -0500 Subject: [PATCH 5/6] Enable `Sorbet/ForbidTEnum` cop Signed-off-by: Alexandre Terrasa --- .rubocop.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.rubocop.yml b/.rubocop.yml index e95c5e02..5ddf14cd 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -32,3 +32,6 @@ Sorbet/TrueSigil: Sorbet/EnforceSigilOrder: Enabled: true + +Sorbet/ForbidTEnum: + Enabled: true From 40f6f8494e39f87dbd7585279be059afda20ea7c Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Thu, 8 Jan 2026 11:49:35 -0500 Subject: [PATCH 6/6] Update exported RBI Signed-off-by: Alexandre Terrasa --- rbi/spoom.rbi | 106 ++++++++++++++++++++++++++++---------------------- 1 file changed, 59 insertions(+), 47 deletions(-) diff --git a/rbi/spoom.rbi b/rbi/spoom.rbi index 13bdac0d..bdafa794 100644 --- a/rbi/spoom.rbi +++ b/rbi/spoom.rbi @@ -85,6 +85,8 @@ module Spoom::Cli::Helper def yellow(string); end end +Spoom::Cli::Helper::HIGHLIGHT_COLOR = T.let(T.unsafe(nil), Spoom::Color) + class Spoom::Cli::Main < ::Thor include ::Spoom::Colorize include ::Spoom::Cli::Helper @@ -212,32 +214,33 @@ Spoom::Cli::Srb::Tc::SORT_CODE = T.let(T.unsafe(nil), String) Spoom::Cli::Srb::Tc::SORT_ENUM = T.let(T.unsafe(nil), Array) Spoom::Cli::Srb::Tc::SORT_LOC = T.let(T.unsafe(nil), String) -class Spoom::Color < ::T::Enum - enums do - BLACK = new - BLUE = new - BOLD = new - CLEAR = new - CYAN = new - GREEN = new - LIGHT_BLACK = new - LIGHT_BLUE = new - LIGHT_CYAN = new - LIGHT_GREEN = new - LIGHT_MAGENTA = new - LIGHT_RED = new - LIGHT_WHITE = new - LIGHT_YELLOW = new - MAGENTA = new - RED = new - WHITE = new - YELLOW = new - end +class Spoom::Color + sig { params(ansi_code: ::String).void } + def initialize(ansi_code); end sig { returns(::String) } def ansi_code; end end +Spoom::Color::BLACK = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::BLUE = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::BOLD = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::CLEAR = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::CYAN = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::GREEN = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::LIGHT_BLACK = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::LIGHT_BLUE = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::LIGHT_CYAN = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::LIGHT_GREEN = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::LIGHT_MAGENTA = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::LIGHT_RED = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::LIGHT_WHITE = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::LIGHT_YELLOW = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::MAGENTA = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::RED = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::WHITE = T.let(T.unsafe(nil), Spoom::Color) +Spoom::Color::YELLOW = T.let(T.unsafe(nil), Spoom::Color) + module Spoom::Colorize sig { params(string: ::String, color: ::Spoom::Color).returns(::String) } def set_color(string, *color); end @@ -999,25 +1002,25 @@ class Spoom::Deadcode::Definition < ::T::Struct def to_json(*args); end end -class Spoom::Deadcode::Definition::Kind < ::T::Enum - enums do - AttrReader = new - AttrWriter = new - Class = new - Constant = new - Method = new - Module = new - end -end +class Spoom::Deadcode::Definition::Kind + sig { params(name: ::String).void } + def initialize(name); end -class Spoom::Deadcode::Definition::Status < ::T::Enum - enums do - ALIVE = new - DEAD = new - IGNORED = new - end + sig { override.returns(::String) } + def to_s; end end +Spoom::Deadcode::Definition::Kind::AttrReader = T.let(T.unsafe(nil), Spoom::Deadcode::Definition::Kind) +Spoom::Deadcode::Definition::Kind::AttrWriter = T.let(T.unsafe(nil), Spoom::Deadcode::Definition::Kind) +Spoom::Deadcode::Definition::Kind::Class = T.let(T.unsafe(nil), Spoom::Deadcode::Definition::Kind) +Spoom::Deadcode::Definition::Kind::Constant = T.let(T.unsafe(nil), Spoom::Deadcode::Definition::Kind) +Spoom::Deadcode::Definition::Kind::Method = T.let(T.unsafe(nil), Spoom::Deadcode::Definition::Kind) +Spoom::Deadcode::Definition::Kind::Module = T.let(T.unsafe(nil), Spoom::Deadcode::Definition::Kind) +class Spoom::Deadcode::Definition::Status; end +Spoom::Deadcode::Definition::Status::ALIVE = T.let(T.unsafe(nil), Spoom::Deadcode::Definition::Status) +Spoom::Deadcode::Definition::Status::DEAD = T.let(T.unsafe(nil), Spoom::Deadcode::Definition::Status) +Spoom::Deadcode::Definition::Status::IGNORED = T.let(T.unsafe(nil), Spoom::Deadcode::Definition::Status) + class Spoom::Deadcode::ERB < ::Erubi::Engine sig { params(input: T.untyped, properties: T.untyped).void } def initialize(input, properties = T.unsafe(nil)); end @@ -2277,13 +2280,14 @@ class Spoom::Model::Reference < ::T::Struct end end -class Spoom::Model::Reference::Kind < ::T::Enum - enums do - Constant = new - Method = new - end +class Spoom::Model::Reference::Kind + sig { params(name: ::String).void } + def initialize(name); end end +Spoom::Model::Reference::Kind::Constant = T.let(T.unsafe(nil), Spoom::Model::Reference::Kind) +Spoom::Model::Reference::Kind::Method = T.let(T.unsafe(nil), Spoom::Model::Reference::Kind) + class Spoom::Model::ReferencesVisitor < ::Spoom::Visitor sig { params(file: ::String).void } def initialize(file); end @@ -2433,14 +2437,22 @@ class Spoom::Model::UnresolvedSymbol < ::Spoom::Model::Symbol def to_s; end end -class Spoom::Model::Visibility < ::T::Enum - enums do - Private = new - Protected = new - Public = new +class Spoom::Model::Visibility + sig { params(name: ::String).void } + def initialize(name); end + + sig { override.returns(::String) } + def to_s; end + + class << self + sig { params(name: ::String).returns(::Spoom::Model::Visibility) } + def from_string(name); end end end +Spoom::Model::Visibility::Private = T.let(T.unsafe(nil), Spoom::Model::Visibility) +Spoom::Model::Visibility::Protected = T.let(T.unsafe(nil), Spoom::Model::Visibility) +Spoom::Model::Visibility::Public = T.let(T.unsafe(nil), Spoom::Model::Visibility) class Spoom::ParseError < ::Spoom::Error; end class Spoom::Poset