rbs_rails 0.12.0 → 0.13.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.github/workflows/release.yml +27 -0
  4. data/CHANGELOG.md +64 -0
  5. data/Gemfile +9 -3
  6. data/Gemfile.lock +202 -129
  7. data/README.md +47 -1
  8. data/Rakefile +7 -2
  9. data/Steepfile +5 -0
  10. data/example/rbs_rails.rb +27 -0
  11. data/exe/rbs_rails +6 -0
  12. data/lib/generators/rbs_rails/install_generator.rb +10 -15
  13. data/lib/rbs_rails/active_record/enum.rb +81 -0
  14. data/lib/rbs_rails/active_record.rb +269 -171
  15. data/lib/rbs_rails/cli/configuration.rb +66 -0
  16. data/lib/rbs_rails/cli.rb +173 -0
  17. data/lib/rbs_rails/dependency_builder.rb +29 -8
  18. data/lib/rbs_rails/path_helpers.rb +14 -2
  19. data/lib/rbs_rails/rake_task.rb +39 -41
  20. data/lib/rbs_rails/util/file_writer.rb +22 -0
  21. data/lib/rbs_rails/util.rb +17 -15
  22. data/lib/rbs_rails/version.rb +1 -1
  23. data/lib/rbs_rails.rb +5 -2
  24. data/rbs_collection.lock.yaml +273 -45
  25. data/rbs_collection.yaml +1 -18
  26. data/rbs_rails.gemspec +2 -1
  27. data/sig/{install_generator.rbs → generators/rbs_rails/install_generator.rbs} +2 -0
  28. data/sig/rbs_rails/active_record/enum.rbs +26 -0
  29. data/sig/rbs_rails/active_record.rbs +68 -46
  30. data/sig/rbs_rails/cli/configuration.rbs +37 -0
  31. data/sig/rbs_rails/cli.rbs +35 -0
  32. data/sig/rbs_rails/dependency_builder.rbs +8 -0
  33. data/sig/rbs_rails/path_helpers.rbs +13 -6
  34. data/sig/rbs_rails/rake_task.rbs +8 -7
  35. data/sig/rbs_rails/util/file_writer.rbs +16 -0
  36. data/sig/rbs_rails/util.rbs +7 -2
  37. data/sig/rbs_rails/utils/file_writer.rbs +4 -0
  38. data/sig/rbs_rails/version.rbs +5 -1
  39. data/sig/rbs_rails.rbs +6 -3
  40. metadata +33 -14
  41. data/sig/_internal/activerecord.rbs +0 -4
  42. data/sig/_internal/fileutils.rbs +0 -4
  43. data/sig/_internal/thor.rbs +0 -5
  44. data/sig/parser.rbs +0 -14
  45. data/sig/rake.rbs +0 -6
@@ -0,0 +1,66 @@
1
+ require 'forwardable'
2
+ require 'singleton'
3
+
4
+ module RbsRails
5
+ class CLI
6
+ class Configuration
7
+ include Singleton
8
+
9
+ # @rbs!
10
+ # def self.instance: () -> Configuration
11
+ # def self.configure: () { (Configuration) -> void } -> void
12
+
13
+ class << self
14
+ extend Forwardable
15
+
16
+ def_delegator :instance, :configure # steep:ignore
17
+ end
18
+
19
+ # @rbs!
20
+ # @signature_root_dir: Pathname?
21
+ # @ignore_model_if: (^(singleton(ActiveRecord::Base)) -> bool)?
22
+
23
+ attr_reader :check_db_migrations #: bool
24
+
25
+ def initialize #: void
26
+ @signature_root_dir = nil
27
+ @ignore_model_if = nil
28
+ @check_db_migrations = true
29
+ end
30
+
31
+ # @rbs &block: (Configuration) -> void
32
+ def configure(&block) #: void
33
+ block.call(self)
34
+ end
35
+
36
+ def signature_root_dir #: Pathname
37
+ @signature_root_dir || Rails.root.join("sig/rbs_rails")
38
+ end
39
+
40
+ # @rbs dir: String | Pathname
41
+ def signature_root_dir=(dir) #: Pathname
42
+ @signature_root_dir = case dir
43
+ when String
44
+ Pathname.new(dir)
45
+ when Pathname
46
+ dir
47
+ else
48
+ raise ArgumentError, "signature_root_dir must be String or Pathname"
49
+ end
50
+ end
51
+
52
+ # @rbs &block: (singleton(ActiveRecord::Base)) -> bool
53
+ def ignore_model_if(&block) #: void
54
+ @ignore_model_if = block
55
+ end
56
+
57
+ # @rbs klass: singleton(ActiveRecord::Base)
58
+ def ignored_model?(klass) #: bool
59
+ ignore_model_if = @ignore_model_if
60
+ return false unless ignore_model_if
61
+
62
+ ignore_model_if.call(klass)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,173 @@
1
+ require "optparse"
2
+ require "rbs_rails/cli/configuration"
3
+
4
+ module RbsRails
5
+ # @rbs &block: (CLI::Configuration) -> void
6
+ def self.configure(&block) #: void
7
+ CLI::Configuration.configure(&block)
8
+ end
9
+
10
+ class CLI
11
+ attr_reader :config_file #: String?
12
+
13
+ # @rbs argv: Array[String]
14
+ def run(argv) #: Integer
15
+ parser = create_option_parser
16
+
17
+ begin
18
+ args = parser.parse(argv)
19
+ subcommand = args.shift || "help"
20
+
21
+ case subcommand
22
+ when "help"
23
+ $stdout.puts parser.help
24
+ 0
25
+ when "version"
26
+ $stdout.puts "rbs_rails #{RbsRails::VERSION}"
27
+ 0
28
+ when "all"
29
+ load_application
30
+ load_config
31
+ generate_models
32
+ generate_path_helpers
33
+ 0
34
+ when "models"
35
+ load_application
36
+ load_config
37
+ generate_models
38
+ 0
39
+ when "path_helpers"
40
+ load_application
41
+ load_config
42
+ generate_path_helpers
43
+ 0
44
+ else
45
+ $stdout.puts "Unknown command: #{subcommand}"
46
+ $stdout.puts parser.help
47
+ 1
48
+ end
49
+ rescue OptionParser::InvalidOption => e
50
+ $stderr.puts "Error: #{e.message}"
51
+ $stdout.puts parser.help
52
+ 1
53
+ end
54
+ rescue StandardError => e
55
+ $stderr.puts "Error: #{e.message}"
56
+ 1
57
+ end
58
+
59
+ private
60
+
61
+ def config #: Configuration
62
+ Configuration.instance
63
+ end
64
+
65
+ def load_config #: void
66
+ if config_file
67
+ load config_file
68
+ else
69
+ if File.exist?(".rbs_rails.rb")
70
+ load ".rbs_rails.rb"
71
+ elsif Rails.root.join("config/rbs_rails.rb").exist?
72
+ load Rails.root.join("config/rbs_rails.rb").to_s
73
+ end
74
+ end
75
+ end
76
+
77
+ def load_application #: void
78
+ require_relative "#{Dir.getwd}/config/application"
79
+
80
+ install_hooks
81
+
82
+ Rails.application.initialize!
83
+ rescue LoadError => e
84
+ raise "Failed to load Rails application: #{e.message}"
85
+ end
86
+
87
+ def install_hooks #: void
88
+ # Load inspectors. This is necessary to load earlier than Rails application.
89
+ require 'rbs_rails/active_record/enum'
90
+ end
91
+
92
+ def generate_models #: void
93
+ check_db_migrations!
94
+ Rails.application.eager_load!
95
+
96
+ ::ActiveRecord::Base.descendants.each do |klass|
97
+ generate_single_model(klass)
98
+ rescue => e
99
+ puts "Error generating RBS for #{klass.name} model"
100
+ raise e
101
+ end
102
+ end
103
+
104
+ # Raise an error if database is not migrated to the latest version
105
+ def check_db_migrations! #: void
106
+ return unless config.check_db_migrations
107
+
108
+ if ::ActiveRecord::Migration.respond_to? :check_all_pending!
109
+ # Rails 7.1 or later
110
+ ::ActiveRecord::Migration.check_all_pending! # steep:ignore NoMethod
111
+ else
112
+ ::ActiveRecord::Migration.check_pending!
113
+ end
114
+ end
115
+
116
+ # @rbs klass: singleton(ActiveRecord::Base)
117
+ def generate_single_model(klass) #: bool
118
+ return false if config.ignored_model?(klass)
119
+ return false unless RbsRails::ActiveRecord.generatable?(klass)
120
+
121
+ original_path, _line = Object.const_source_location(klass.name) rescue nil
122
+
123
+ rbs_relative_path = if original_path && Pathname.new(original_path).fnmatch?("#{Rails.root}/**")
124
+ Pathname.new(original_path)
125
+ .relative_path_from(Rails.root)
126
+ .sub_ext('.rbs')
127
+ else
128
+ "app/models/#{klass.name.underscore}.rbs"
129
+ end
130
+
131
+ path = config.signature_root_dir / rbs_relative_path
132
+ path.dirname.mkpath
133
+
134
+ sig = RbsRails::ActiveRecord.class_to_rbs(klass)
135
+ Util::FileWriter.new(path).write sig
136
+
137
+ true
138
+ end
139
+
140
+ def generate_path_helpers #: void
141
+ path = config.signature_root_dir.join 'path_helpers.rbs'
142
+ path.dirname.mkpath
143
+
144
+ sig = RbsRails::PathHelpers.generate
145
+ Util::FileWriter.new(path).write sig
146
+ end
147
+
148
+ def create_option_parser #: OptionParser
149
+ OptionParser.new do |opts|
150
+ opts.banner = <<~BANNER
151
+ Usage: rbs_rails [command] [options]
152
+
153
+ Commands:
154
+ help Show this help message
155
+ version Show version
156
+ all Generate all RBS files
157
+ models Generate RBS files for models
158
+ path_helpers Generate RBS for Rails path helpers
159
+
160
+ Options:
161
+ BANNER
162
+
163
+ opts.on("--signature-root-dir=DIR", "Specify the root directory for RBS signatures") do |dir|
164
+ config.signature_root_dir = Pathname.new(dir)
165
+ end
166
+
167
+ opts.on("--config=FILE", "Load configuration from FILE") do |file|
168
+ @config_file = file
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -1,26 +1,43 @@
1
1
  module RbsRails
2
2
  class DependencyBuilder
3
- attr_reader :deps, :done
3
+ attr_reader :deps #: Array[String]
4
+ attr_reader :done #: Set[String]
4
5
 
5
- def initialize
6
+ def initialize #: void
6
7
  @deps = []
7
- @done = Set.new(['ActiveRecord::Base', 'ActiveRecord', 'Object'])
8
+ @done = Set.new([
9
+ 'ActiveRecord',
10
+ 'ActiveRecord::Associations',
11
+ 'ActiveRecord::Associations::CollectionProxy',
12
+ 'ActiveRecord::Base',
13
+ 'ActiveRecord::Relation',
14
+ 'ActiveStorage',
15
+ 'ActiveStorage::Attachment',
16
+ 'ActiveStorage::Blob',
17
+ 'ActiveStorage::Record',
18
+ 'Object'
19
+ ])
8
20
  end
9
21
 
10
- def build
22
+ # @rbs name: String
23
+ def <<(name) #: Array[String]
24
+ deps << name
25
+ end
26
+
27
+ def build #: String | nil
11
28
  dep_rbs = +""
12
29
  deps.uniq!
13
- while dep = deps.shift
30
+ while dep = shift
14
31
  next unless done.add?(dep)
15
32
 
16
33
  case dep_object = Object.const_get(dep)
17
34
  when Class
18
35
  superclass = dep_object.superclass or raise
19
- super_name = Util.module_name(superclass)
36
+ super_name = Util.module_name(superclass, abs: false)
20
37
  deps << super_name
21
- dep_rbs << "class #{dep} < #{super_name} end\n"
38
+ dep_rbs << "class ::#{dep} < ::#{super_name} end\n"
22
39
  when Module
23
- dep_rbs << "module #{dep} end\n"
40
+ dep_rbs << "module ::#{dep} end\n"
24
41
  else
25
42
  raise
26
43
  end
@@ -39,5 +56,9 @@ module RbsRails
39
56
  Util.format_rbs(dep_rbs)
40
57
  end
41
58
  end
59
+
60
+ private def shift #: String | nil
61
+ deps.shift&.sub(/^::/, '')
62
+ end
42
63
  end
43
64
  end
@@ -1,6 +1,10 @@
1
1
  module RbsRails
2
2
  class PathHelpers
3
3
  def self.generate(routes: Rails.application.routes)
4
+ # Since Rails 8.0, route drawing has been deferred to the first request.
5
+ # This forcedly loads routes before generating path_helpers.
6
+ Rails.application.routes.eager_load!
7
+
4
8
  new(routes: Rails.application.routes).generate
5
9
  end
6
10
 
@@ -11,13 +15,21 @@ module RbsRails
11
15
  def generate
12
16
  methods = helpers.map do |helper|
13
17
  # TODO: More restrict argument types
14
- "def #{helper}: (*untyped) -> String"
18
+ "def #{helper}: (*untyped) -> ::String"
15
19
  end
16
20
 
17
21
  <<~RBS
18
- interface _RbsRailsPathHelpers
22
+ # resolve-type-names: false
23
+
24
+ interface ::_RbsRailsPathHelpers
19
25
  #{methods.join("\n").indent(2)}
20
26
  end
27
+
28
+ module ::ActionController
29
+ class ::ActionController::Base
30
+ include ::_RbsRailsPathHelpers
31
+ end
32
+ end
21
33
  RBS
22
34
  end
23
35
 
@@ -3,71 +3,69 @@ require 'rake/tasklib'
3
3
 
4
4
  module RbsRails
5
5
  class RakeTask < Rake::TaskLib
6
- attr_accessor :ignore_model_if, :name, :signature_root_dir
7
-
8
- def initialize(name = :rbs_rails, &block)
6
+ # @rbs!
7
+ # interface _Filter
8
+ # def call: (Class) -> boolish
9
+ # end
10
+
11
+ attr_accessor :ignore_model_if #: _Filter | nil
12
+ attr_accessor :name #: Symbol
13
+ attr_writer :signature_root_dir #: Pathname?
14
+
15
+ # @rbs name: ::Symbol
16
+ # @rbs &block: (RbsRails::RakeTask) -> void
17
+ def initialize(name = :rbs_rails, &block) #: void
9
18
  super()
10
19
 
11
20
  @name = name
21
+ @signature_root_dir = nil
12
22
 
13
23
  block.call(self) if block
14
24
 
15
- setup_signature_root_dir!
16
-
17
25
  def_generate_rbs_for_models
18
26
  def_generate_rbs_for_path_helpers
19
27
  def_all
20
28
  end
21
29
 
22
- def def_all
30
+ def def_all #: void
23
31
  desc 'Run all tasks of rbs_rails'
24
-
25
- deps = [:"#{name}:generate_rbs_for_models", :"#{name}:generate_rbs_for_path_helpers"]
26
- task("#{name}:all": deps)
32
+ task :"#{name}:all" do
33
+ if signature_root_dir
34
+ sh "rbs_rails", "all", "--signature-root-dir=#{signature_root_dir}"
35
+ else
36
+ sh "rbs_rails", "all"
37
+ end
38
+ end
27
39
  end
28
40
 
29
- def def_generate_rbs_for_models
41
+ def def_generate_rbs_for_models #: void
30
42
  desc 'Generate RBS files for Active Record models'
31
- task("#{name}:generate_rbs_for_models": :environment) do
32
- require 'rbs_rails'
33
-
34
- Rails.application.eager_load!
35
-
36
- dep_builder = DependencyBuilder.new
37
-
38
- ::ActiveRecord::Base.descendants.each do |klass|
39
- next unless RbsRails::ActiveRecord.generatable?(klass)
40
- next if ignore_model_if&.call(klass)
43
+ task :"#{name}:generate_rbs_for_models" do
44
+ warn "ignore_model_if is deprecated." if ignore_model_if
41
45
 
42
- path = signature_root_dir / "app/models/#{klass.name.underscore}.rbs"
43
- path.dirname.mkpath
44
-
45
- sig = RbsRails::ActiveRecord.class_to_rbs(klass, dependencies: dep_builder.deps)
46
- path.write sig
47
- dep_builder.done << klass.name
48
- end
49
-
50
- if dep_rbs = dep_builder.build
51
- signature_root_dir.join('model_dependencies.rbs').write(dep_rbs)
46
+ if signature_root_dir
47
+ sh "rbs_rails", "models", "--signature-root-dir=#{signature_root_dir}"
48
+ else
49
+ sh "rbs_rails", "models"
52
50
  end
53
51
  end
54
52
  end
55
53
 
56
- def def_generate_rbs_for_path_helpers
54
+ def def_generate_rbs_for_path_helpers #: void
57
55
  desc 'Generate RBS files for path helpers'
58
- task("#{name}:generate_rbs_for_path_helpers": :environment) do
59
- require 'rbs_rails'
60
-
61
- out_path = signature_root_dir.join 'path_helpers.rbs'
62
- rbs = RbsRails::PathHelpers.generate
63
- out_path.write rbs
56
+ task :"#{name}:generate_rbs_for_path_helpers" do
57
+ if signature_root_dir
58
+ sh "rbs_rails", "path_helpers", "--signature-root-dir=#{signature_root_dir}"
59
+ else
60
+ sh "rbs_rails", "path_helpers"
61
+ end
64
62
  end
65
63
  end
66
64
 
67
- private def setup_signature_root_dir!
68
- @signature_root_dir ||= Rails.root / 'sig/rbs_rails'
69
- @signature_root_dir = Pathname(@signature_root_dir)
70
- @signature_root_dir.mkpath
65
+ private def signature_root_dir #: Pathname?
66
+ if path = @signature_root_dir
67
+ Pathname(path)
68
+ end
71
69
  end
72
70
  end
73
71
  end
@@ -0,0 +1,22 @@
1
+ module RbsRails
2
+ module Util
3
+ # To avoid unnecessary type reloading by type checkers and other utilities,
4
+ # FileWriter modifies the target file only if its content has been changed.
5
+ class FileWriter
6
+ attr_reader :path #: Pathname
7
+
8
+ # @rbs path: Pathname
9
+ def initialize(path) #: void
10
+ @path = path
11
+ end
12
+
13
+ def write(content) #: void
14
+ original_content = path.read rescue nil
15
+
16
+ if original_content != content
17
+ path.write(content)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,29 +1,31 @@
1
+ require_relative 'util/file_writer'
2
+
1
3
  module RbsRails
2
4
  module Util
3
- MODULE_NAME = Module.instance_method(:name)
5
+ MODULE_NAME = Module.instance_method(:name) #: UnboundMethod
6
+
7
+ # @rbs! extend Util
4
8
 
5
9
  extend self
6
10
 
7
- if '2.7' <= RUBY_VERSION
8
- def module_name(mod)
9
- # HACK: RBS doesn't have UnboundMethod#bind_call
10
- (_ = MODULE_NAME).bind_call(mod)
11
- end
12
- else
13
- def module_name(mod)
14
- MODULE_NAME.bind(mod).call
15
- end
11
+ # @rbs mod: Module
12
+ # @rbs abs: boolish
13
+ def module_name(mod, abs: true) #: String
14
+ name = MODULE_NAME.bind_call(mod)
15
+ name ="::#{name}" if abs
16
+ name
16
17
  end
17
18
 
18
- def format_rbs(rbs)
19
+ # @rbs rbs: String
20
+ def format_rbs(rbs) #: String
19
21
  decls =
20
- if Gem::Version.new('3') <= Gem::Version.new(RBS::VERSION)
21
- # TODO: Remove this type annotation when rbs_rails depends on RBS v3
22
- # @type var parsed: [RBS::Buffer, untyped, RBS::Declarations::t]
22
+ if Gem::Version.new('3') <= Gem::Version.new(RBS::VERSION)
23
23
  parsed = _ = RBS::Parser.parse_signature(rbs)
24
24
  parsed[1] + parsed[2]
25
25
  else
26
- RBS::Parser.parse_signature(rbs)
26
+ # TODO: Remove this type annotation when rbs_rails drops support of RBS 2.x.
27
+ # @type var parsed: [RBS::Declarations::t]
28
+ parsed = _ = RBS::Parser.parse_signature(rbs)
27
29
  end
28
30
 
29
31
  StringIO.new.tap do |io|
@@ -2,5 +2,5 @@ module RbsRails
2
2
  # Because of copy_signatures is defined by lib/rbs_rails.rb
3
3
  # @dynamic self.copy_signatures
4
4
 
5
- VERSION = "0.12.0"
5
+ VERSION = "0.13.0" #: String
6
6
  end
data/lib/rbs_rails.rb CHANGED
@@ -1,17 +1,20 @@
1
- require 'parser/current'
1
+ require 'parser'
2
+ require 'prism'
2
3
  require 'rbs'
3
4
  require 'stringio'
4
5
 
5
6
  require_relative "rbs_rails/version"
6
7
  require_relative "rbs_rails/util"
7
8
  require_relative 'rbs_rails/active_record'
9
+ require_relative 'rbs_rails/active_record/enum'
8
10
  require_relative 'rbs_rails/path_helpers'
9
11
  require_relative 'rbs_rails/dependency_builder'
10
12
 
11
13
  module RbsRails
12
14
  class Error < StandardError; end
13
15
 
14
- def self.copy_signatures(to:)
16
+ # @rbs to: untyped
17
+ def self.copy_signatures(to:) #: untyped
15
18
  from = Pathname(_ = __dir__) / '../assets/sig/'
16
19
  to = Pathname(to)
17
20
  FileUtils.cp_r(from, to)