tapioca 0.5.0 → 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
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) }