tapioca 0.5.0 → 0.5.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f062008a7e93aa3119ee191492917febf4a97294753865bb58ae0bac91e582d8
4
- data.tar.gz: d4b89c7368da54276c54977ae742ce1765178ac6700193a9aefb5c1e65bc9dcd
3
+ metadata.gz: 28c385b223b0ab1221b81420ad3a40222ff243552e121959e1e2290b1711357e
4
+ data.tar.gz: 1c06cd621dee12c9afd506a669c2762d2c7ca2f471f2a22b4e38bde6092f9f13
5
5
  SHA512:
6
- metadata.gz: 4466416f851f4e6a7e8342a8dae5229e38ede32da3823c0a7cdd97dc3bff6a3e3711d983d5552fc083725789bb3b0a6899b4cf2edcc5c855bc33224054598987
7
- data.tar.gz: d66b2a55050aae7089d7349e9c4e43cc1a2e2c1f181805f5c9d6b1a1e2c26c3b3c7b9d939efc0580f4d83cde4c615dd13e4d40f30682c2cfc441fde58e22f23a
6
+ metadata.gz: e8f6278d058bef31b5fdac1d94556814de7486355d1bcdf9fb2e29da6f33743ec5c330befb492b51ada82d392d10bae2e0c663eda7a365c1272631add49adce6
7
+ data.tar.gz: 4a342639d1ab6a79b83f6273ac9d44589919953f70773d0492c8c038250f344541d280801db88be72883dd8c55ff8ccd9fafa9ffcbff32e5421df2e821b59365
data/Gemfile CHANGED
@@ -11,7 +11,6 @@ gem("pry-byebug")
11
11
  gem("rubocop-shopify", require: false)
12
12
  gem("rubocop-sorbet", ">= 0.4.1")
13
13
  gem("sorbet")
14
- gem("yard", "~> 0.9.25")
15
14
 
16
15
  group(:deployment, :development) do
17
16
  gem("rake")
@@ -35,4 +34,5 @@ group(:development, :test) do
35
34
  gem("nokogiri", require: false)
36
35
  gem("config", require: false)
37
36
  gem("aasm", require: false)
37
+ gem("bcrypt", require: false)
38
38
  end
data/lib/tapioca/cli.rb CHANGED
@@ -5,8 +5,6 @@ require "thor"
5
5
 
6
6
  module Tapioca
7
7
  class Cli < Thor
8
- include(Thor::Actions)
9
-
10
8
  class_option :outdir,
11
9
  aliases: ["--out", "-o"],
12
10
  banner: "directory",
@@ -29,22 +27,37 @@ module Tapioca
29
27
 
30
28
  desc "init", "initializes folder structure"
31
29
  def init
32
- create_config
33
- create_post_require
34
- generate_binstub
30
+ generator = Generators::Init.new(
31
+ sorbet_config: Config::SORBET_CONFIG,
32
+ default_postrequire: Config::DEFAULT_POSTREQUIRE,
33
+ default_command: Config::DEFAULT_COMMAND
34
+ )
35
+ generator.generate
35
36
  end
36
37
 
37
38
  desc "require", "generate the list of files to be required by tapioca"
38
39
  def require
40
+ generator = Generators::Require.new(
41
+ requires_path: ConfigBuilder.from_options(:require, options).postrequire,
42
+ sorbet_config_path: Config::SORBET_CONFIG,
43
+ default_command: Config::DEFAULT_COMMAND
44
+ )
39
45
  Tapioca.silence_warnings do
40
- generator.build_requires
46
+ generator.generate
41
47
  end
42
48
  end
43
49
 
44
50
  desc "todo", "generate the list of unresolved constants"
45
51
  def todo
52
+ current_command = T.must(current_command_chain.first)
53
+ config = ConfigBuilder.from_options(current_command, options)
54
+ generator = Generators::Todo.new(
55
+ todos_path: config.todos_path,
56
+ file_header: config.file_header,
57
+ default_command: Config::DEFAULT_COMMAND
58
+ )
46
59
  Tapioca.silence_warnings do
47
- generator.build_todos
60
+ generator.generate
48
61
  end
49
62
  end
50
63
 
@@ -67,13 +80,23 @@ module Tapioca
67
80
  type: :boolean,
68
81
  desc: "Supresses file creation output"
69
82
  def dsl(*constants)
83
+ current_command = T.must(current_command_chain.first)
84
+ config = ConfigBuilder.from_options(current_command, options)
85
+ generator = Generators::Dsl.new(
86
+ requested_constants: constants,
87
+ outpath: config.outpath,
88
+ generators: config.generators,
89
+ exclude_generators: config.exclude_generators,
90
+ file_header: config.file_header,
91
+ compiler_path: Tapioca::Compilers::Dsl::COMPILERS_PATH,
92
+ tapioca_path: Config::TAPIOCA_PATH,
93
+ default_command: Config::DEFAULT_COMMAND,
94
+ should_verify: options[:verify],
95
+ quiet: options[:quiet],
96
+ verbose: options[:verbose]
97
+ )
70
98
  Tapioca.silence_warnings do
71
- generator.build_dsl(
72
- constants,
73
- should_verify: options[:verify],
74
- quiet: options[:quiet],
75
- verbose: options[:verbose]
76
- )
99
+ generator.generate
77
100
  end
78
101
  end
79
102
 
@@ -104,10 +127,27 @@ module Tapioca
104
127
  type: :boolean,
105
128
  default: false,
106
129
  desc: "Verifies RBIs are up-to-date"
130
+ option :doc,
131
+ type: :boolean,
132
+ default: false,
133
+ desc: "Include YARD documentation from sources when generating RBIs. Warning: this might be slow"
107
134
  def gem(*gems)
108
135
  Tapioca.silence_warnings do
109
136
  all = options[:all]
110
137
  verify = options[:verify]
138
+ current_command = T.must(current_command_chain.first)
139
+ config = ConfigBuilder.from_options(current_command, options)
140
+ generator = Generators::Gem.new(
141
+ gem_names: all ? [] : gems,
142
+ gem_excludes: config.exclude,
143
+ prerequire: config.prerequire,
144
+ postrequire: config.postrequire,
145
+ typed_overrides: config.typed_overrides,
146
+ default_command: Config::DEFAULT_COMMAND,
147
+ outpath: config.outpath,
148
+ file_header: config.file_header,
149
+ doc: config.doc
150
+ )
111
151
 
112
152
  raise MalformattedArgumentError, "Options '--all' and '--verify' are mutually exclusive" if all && verify
113
153
 
@@ -117,146 +157,22 @@ module Tapioca
117
157
  end
118
158
 
119
159
  if gems.empty? && !all
120
- generator.sync_rbis_with_gemfile(should_verify: verify)
160
+ generator.sync(should_verify: verify)
121
161
  else
122
- generator.build_gem_rbis(all ? [] : gems)
162
+ generator.generate
123
163
  end
124
164
  end
125
165
  end
126
166
 
127
- desc "generate [gem...]", "DEPRECATED: generate RBIs from gems"
128
- option :prerequire,
129
- aliases: ["--pre", "-b"],
130
- banner: "file",
131
- desc: "A file to be required before Bundler.require is called"
132
- option :postrequire,
133
- aliases: ["--post", "-a"],
134
- banner: "file",
135
- desc: "A file to be required after Bundler.require is called"
136
- option :exclude,
137
- aliases: ["-x"],
138
- type: :array,
139
- banner: "gem [gem ...]",
140
- desc: "Excludes the given gem(s) from RBI generation"
141
- option :typed_overrides,
142
- aliases: ["--typed", "-t"],
143
- type: :hash,
144
- banner: "gem:level [gem:level ...]",
145
- desc: "Overrides for typed sigils for generated gem RBIs"
146
- def generate(*gems)
147
- gem_names = if gems.empty?
148
- "--all"
149
- else
150
- gems.join(" ")
151
- end
152
- deprecation_message = <<~MSG
153
- DEPRECATION: The `generate` command will be removed in a future release.
154
-
155
- Start using `bin/tapioca gem #{gem_names}` instead.
156
- MSG
157
-
158
- say(deprecation_message, :red)
159
- say("")
160
-
161
- Tapioca.silence_warnings do
162
- generator.build_gem_rbis(gems)
163
- end
164
-
165
- say("")
166
- say(deprecation_message, :red)
167
- end
168
-
169
- desc "sync", "DEPRECATED: sync RBIs to Gemfile"
170
- option :prerequire,
171
- aliases: ["--pre", "-b"],
172
- banner: "file",
173
- desc: "A file to be required before Bundler.require is called"
174
- option :postrequire,
175
- aliases: ["--post", "-a"],
176
- banner: "file",
177
- desc: "A file to be required after Bundler.require is called"
178
- option :exclude,
179
- aliases: ["-x"],
180
- type: :array,
181
- banner: "gem [gem ...]",
182
- desc: "Excludes the given gem(s) from RBI generation"
183
- option :typed_overrides,
184
- aliases: ["--typed", "-t"],
185
- type: :hash,
186
- banner: "gem:level [gem:level ...]",
187
- desc: "Overrides for typed sigils for generated gem RBIs"
188
- option :verify,
189
- type: :boolean,
190
- default: false,
191
- desc: "Verifies RBIs are up-to-date"
192
- def sync
193
- deprecation_message = <<~MSG
194
- DEPRECATION: The `sync` command will be removed in a future release.
195
-
196
- Start using `bin/tapioca gem` instead.
197
- MSG
198
-
199
- say(deprecation_message, :red)
200
- say("")
201
-
202
- Tapioca.silence_warnings do
203
- generator.sync_rbis_with_gemfile(should_verify: options[:verify])
204
- end
205
-
206
- say("")
207
- say(deprecation_message, :red)
208
- end
209
-
210
167
  desc "--version, -v", "show version"
211
168
  def __print_version
212
169
  puts "Tapioca v#{Tapioca::VERSION}"
213
170
  end
214
171
 
215
- private
216
-
217
- def create_config
218
- create_file(Config::SORBET_CONFIG, skip: true) do
219
- <<~CONTENT
220
- --dir
221
- .
222
- CONTENT
223
- end
224
- end
225
-
226
- def create_post_require
227
- create_file(Config::DEFAULT_POSTREQUIRE, skip: true) do
228
- <<~CONTENT
229
- # typed: true
230
- # frozen_string_literal: true
231
-
232
- # Add your extra requires here (`tapioca require` can be used to boostrap this list)
233
- CONTENT
234
- end
235
- end
236
-
237
- def generate_binstub
238
- bin_stub_exists = File.exist?("bin/tapioca")
239
- installer = Bundler::Installer.new(Bundler.root, Bundler.definition)
240
- spec = Bundler.definition.specs.find { |s| s.name == "tapioca" }
241
- installer.generate_bundler_executable_stubs(spec, { force: true })
242
- if bin_stub_exists
243
- shell.say_status(:force, "bin/tapioca", :yellow)
244
- else
245
- shell.say_status(:create, "bin/tapioca", :green)
246
- end
247
- end
248
-
249
172
  no_commands do
250
173
  def self.exit_on_failure?
251
174
  true
252
175
  end
253
-
254
- def generator
255
- current_command = T.must(current_command_chain.first)
256
- @generator ||= Generator.new(
257
- ConfigBuilder.from_options(current_command, options)
258
- )
259
- end
260
176
  end
261
177
  end
262
178
  end
@@ -0,0 +1,101 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "active_model"
6
+ rescue LoadError
7
+ return
8
+ end
9
+
10
+ module Tapioca
11
+ module Compilers
12
+ module Dsl
13
+ # `Tapioca::Compilers::Dsl::ActiveModelSecurePassword` decorates RBI files for all
14
+ # classes that use [`ActiveModel::SecurePassword`](http://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html).
15
+ #
16
+ # For example, with the following class:
17
+ #
18
+ # ~~~rb
19
+ # class User
20
+ # include ActiveModel::SecurePassword
21
+ #
22
+ # has_secure_password
23
+ # has_secure_password :token
24
+ # end
25
+ # ~~~
26
+ #
27
+ # this generator will produce an RBI file with the following content:
28
+ # ~~~rbi
29
+ # # typed: true
30
+ #
31
+ # class User
32
+ # sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
33
+ # def authenticate(unencrypted_password); end
34
+ #
35
+ # sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
36
+ # def authenticate_password(unencrypted_password); end
37
+ #
38
+ # sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
39
+ # def authenticate_token(unencrypted_password); end
40
+ #
41
+ # sig { returns(T.untyped) }
42
+ # def password; end
43
+ #
44
+ # sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
45
+ # def password=(unencrypted_password); end
46
+ #
47
+ # sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
48
+ # def password_confirmation=(unencrypted_password); end
49
+ #
50
+ # sig { returns(T.untyped) }
51
+ # def token; end
52
+ #
53
+ # sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
54
+ # def token=(unencrypted_password); end
55
+ #
56
+ # sig { params(unencrypted_password: T.untyped).returns(T.untyped) }
57
+ # def token_confirmation=(unencrypted_password); end
58
+ # end
59
+ # ~~~
60
+ class ActiveModelSecurePassword < Base
61
+ extend T::Sig
62
+
63
+ sig do
64
+ override
65
+ .params(root: RBI::Tree, constant: T.all(Class, ::ActiveModel::SecurePassword::ClassMethods))
66
+ .void
67
+ end
68
+ def decorate(root, constant)
69
+ instance_methods_modules = if constant < ActiveModel::SecurePassword::InstanceMethodsOnActivation
70
+ # pre Rails 6.0, this used to be a single static module
71
+ [ActiveModel::SecurePassword::InstanceMethodsOnActivation]
72
+ else
73
+ # post Rails 6.0, this is now using a dynmaic module builder pattern
74
+ # and we can have multiple different ones included into the model
75
+ constant.ancestors.grep(ActiveModel::SecurePassword::InstanceMethodsOnActivation)
76
+ end
77
+
78
+ return if instance_methods_modules.empty?
79
+
80
+ methods = instance_methods_modules.flat_map { |mod| mod.instance_methods(false) }
81
+ return if methods.empty?
82
+
83
+ root.create_path(constant) do |klass|
84
+ methods.each do |method|
85
+ create_method_from_def(klass, constant.instance_method(method))
86
+ end
87
+ end
88
+ end
89
+
90
+ sig { override.returns(T::Enumerable[Module]) }
91
+ def gather_constants
92
+ # This selects all classes that are `ActiveModel::SecurePassword::ClassMethods === klass`.
93
+ # In other words, we select all classes that have `ActiveModel::SecurePassword::ClassMethods`
94
+ # as an ancestor of its singleton class, i.e. all classes that have extended the
95
+ # `ActiveModel::SecurePassword::ClassMethods` module.
96
+ all_classes.grep(::ActiveModel::SecurePassword::ClassMethods)
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,86 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "rails"
6
+ require "active_record"
7
+ require "active_record/fixtures"
8
+ require "active_support/test_case"
9
+ rescue LoadError
10
+ return
11
+ end
12
+
13
+ module Tapioca
14
+ module Compilers
15
+ module Dsl
16
+ # `Tapioca::Compilers::Dsl::ActiveRecordFixtures` decorates RBIs for test fixture methods
17
+ # that are created dynamically by Rails.
18
+ #
19
+ # For example, given an application with a posts table, we can have a fixture file
20
+ #
21
+ # ~~~yaml
22
+ # first_post:
23
+ # author: John
24
+ # title: My post
25
+ # ~~~
26
+ #
27
+ # Rails will allow us to invoke `posts(:first_post)` in tests to get the fixture record.
28
+ # The generated RBI by this generator will produce the following
29
+ #
30
+ # ~~~rbi
31
+ # # test_case.rbi
32
+ # # typed: true
33
+ # class ActiveSupport::TestCase
34
+ # sig { params(fixture_names: Symbol).returns(T.untyped) }
35
+ # def posts(*fixture_names); end
36
+ # end
37
+ # ~~~
38
+ class ActiveRecordFixtures < Base
39
+ extend T::Sig
40
+
41
+ sig { override.params(root: RBI::Tree, constant: T.class_of(ActiveSupport::TestCase)).void }
42
+ def decorate(root, constant)
43
+ method_names = fixture_loader.ancestors # get all ancestors from class that includes AR fixtures
44
+ .drop(1) # drop the anonymous class itself from the array
45
+ .reject(&:name) # only collect anonymous ancestors because fixture methods are always on an anonymous module
46
+ .map! do |mod|
47
+ [mod.private_instance_methods(false), mod.instance_methods(false)]
48
+ end
49
+ .flatten # merge methods into a single list
50
+ return if method_names.empty?
51
+
52
+ root.create_path(constant) do |mod|
53
+ method_names.each do |name|
54
+ create_fixture_method(mod, name.to_s)
55
+ end
56
+ end
57
+ end
58
+
59
+ sig { override.returns(T::Enumerable[Module]) }
60
+ def gather_constants
61
+ [ActiveSupport::TestCase]
62
+ end
63
+
64
+ private
65
+
66
+ sig { returns(Class) }
67
+ def fixture_loader
68
+ Class.new do
69
+ T.unsafe(self).include(ActiveRecord::TestFixtures)
70
+ T.unsafe(self).fixture_path = Rails.root.join("test", "fixtures")
71
+ T.unsafe(self).fixtures(:all)
72
+ end
73
+ end
74
+
75
+ sig { params(mod: RBI::Scope, name: String).void }
76
+ def create_fixture_method(mod, name)
77
+ mod.create_method(
78
+ name,
79
+ parameters: [create_rest_param("fixture_names", type: "Symbol")],
80
+ return_type: "T.untyped"
81
+ )
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -9,8 +9,6 @@ rescue LoadError
9
9
  return
10
10
  end
11
11
 
12
- return unless Tapioca::Compilers::Sorbet.supports?(:mixes_in_class_methods_multiple_args)
13
-
14
12
  module Tapioca
15
13
  module Compilers
16
14
  module Dsl
@@ -6,6 +6,8 @@ require "tapioca/rbi_ext/model"
6
6
  module Tapioca
7
7
  module Compilers
8
8
  module Dsl
9
+ COMPILERS_PATH = T.let(File.expand_path("..", __FILE__).to_s, String)
10
+
9
11
  class Base
10
12
  extend T::Sig
11
13
  extend T::Helpers
@@ -17,9 +19,13 @@ module Tapioca
17
19
  sig { returns(T::Set[Module]) }
18
20
  attr_reader :processable_constants
19
21
 
22
+ sig { returns(T::Array[String]) }
23
+ attr_reader :errors
24
+
20
25
  sig { void }
21
26
  def initialize
22
27
  @processable_constants = T.let(Set.new(gather_constants), T::Set[Module])
28
+ @errors = T.let([], T::Array[String])
23
29
  end
24
30
 
25
31
  sig { params(constant: Module).returns(T::Boolean) }
@@ -41,6 +47,12 @@ module Tapioca
41
47
  sig { abstract.returns(T::Enumerable[Module]) }
42
48
  def gather_constants; end
43
49
 
50
+ # NOTE: This should eventually accept an `Error` object or `Exception` rather than simply a `String`.
51
+ sig { params(error: String).void }
52
+ def add_error(error)
53
+ @errors << error
54
+ end
55
+
44
56
  private
45
57
 
46
58
  sig { returns(T::Enumerable[Class]) }
@@ -0,0 +1,74 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require "active_support/core_ext/class/attribute"
6
+ rescue LoadError
7
+ return
8
+ end
9
+
10
+ module Tapioca
11
+ module Compilers
12
+ module Dsl
13
+ # `Tapioca::Compilers::Dsl::MixedInClassAttributes` generates RBI files for modules that dynamically use
14
+ # `class_attribute` on classes.
15
+ #
16
+ # For example, given the following concern
17
+ #
18
+ # ~~~rb
19
+ # module Taggeable
20
+ # extend ActiveSupport::Concern
21
+ #
22
+ # included do
23
+ # class_attribute :tag
24
+ # end
25
+ # end
26
+ # ~~~
27
+ #
28
+ # this generator will produce the RBI file `taggeable.rbi` with the following content:
29
+ #
30
+ # ~~~rbi
31
+ # # typed: strong
32
+ #
33
+ # module Taggeable
34
+ # include GeneratedInstanceMethods
35
+ #
36
+ # mixes_in_class_methods GeneratedClassMethods
37
+ #
38
+ # module GeneratedClassMethods
39
+ # def tag; end
40
+ # def tag=(value); end
41
+ # def tag?; end
42
+ # end
43
+ #
44
+ # module GeneratedInstanceMethods
45
+ # def tag; end
46
+ # def tag=(value); end
47
+ # def tag?; end
48
+ # end
49
+ # end
50
+ # ~~~
51
+ class MixedInClassAttributes < Base
52
+ extend T::Sig
53
+
54
+ sig { override.params(root: RBI::Tree, constant: Module).void }
55
+ def decorate(root, constant)
56
+ mixin_compiler = DynamicMixinCompiler.new(constant)
57
+ return if mixin_compiler.empty_attributes?
58
+
59
+ root.create_path(constant) do |mod|
60
+ mixin_compiler.compile_class_attributes(mod)
61
+ end
62
+ end
63
+
64
+ sig { override.returns(T::Enumerable[Module]) }
65
+ def gather_constants
66
+ # Select all non-anonymous modules that have overridden Module.included
67
+ all_modules.select do |mod|
68
+ !mod.is_a?(Class) && name_of(mod) && Tapioca::Reflection.method_of(mod, :included).owner != Module
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -84,10 +84,10 @@ module Tapioca
84
84
 
85
85
  sig { override.returns(T::Enumerable[Module]) }
86
86
  def gather_constants
87
- all_classes.select do |c|
88
- c < ::SmartProperties
89
- end.reject do |c|
90
- name_of(c).nil? || c == ::SmartProperties::Validations::Ancestor
87
+ all_modules.select do |c|
88
+ name_of(c) &&
89
+ c != ::SmartProperties::Validations::Ancestor &&
90
+ c < ::SmartProperties && ::SmartProperties::ClassMethods === c
91
91
  end
92
92
  end
93
93
 
@@ -34,7 +34,7 @@ module Tapioca
34
34
  @error_handler = T.let(error_handler || $stderr.method(:puts), T.proc.params(error: String).void)
35
35
  end
36
36
 
37
- sig { params(blk: T.proc.params(constant: Module, rbi: String).void).void }
37
+ sig { params(blk: T.proc.params(constant: Module, rbi: RBI::File).void).void }
38
38
  def run(&blk)
39
39
  constants_to_process = gather_constants(requested_constants)
40
40
 
@@ -51,6 +51,10 @@ module Tapioca
51
51
 
52
52
  blk.call(constant, rbi)
53
53
  end
54
+
55
+ generators.flat_map(&:errors).each do |msg|
56
+ report_error(msg)
57
+ end
54
58
  end
55
59
 
56
60
  private
@@ -77,7 +81,7 @@ module Tapioca
77
81
  constants
78
82
  end
79
83
 
80
- sig { params(constant: Module).returns(T.nilable(String)) }
84
+ sig { params(constant: Module).returns(T.nilable(RBI::File)) }
81
85
  def rbi_for_constant(constant)
82
86
  file = RBI::File.new(strictness: "true")
83
87
 
@@ -88,10 +92,7 @@ module Tapioca
88
92
 
89
93
  return if file.root.empty?
90
94
 
91
- file.root.nest_non_public_methods!
92
- file.root.group_nodes!
93
- file.root.sort_nodes!
94
- file.string
95
+ file
95
96
  end
96
97
 
97
98
  sig { params(error: String).returns(T.noreturn) }