tapioca 0.2.7 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +27 -1
  3. data/README.md +21 -2
  4. data/Rakefile +15 -4
  5. data/lib/tapioca.rb +15 -9
  6. data/lib/tapioca/cli.rb +41 -12
  7. data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +129 -0
  8. data/lib/tapioca/compilers/dsl/action_mailer.rb +65 -0
  9. data/lib/tapioca/compilers/dsl/active_record_associations.rb +285 -0
  10. data/lib/tapioca/compilers/dsl/active_record_columns.rb +379 -0
  11. data/lib/tapioca/compilers/dsl/active_record_enum.rb +112 -0
  12. data/lib/tapioca/compilers/dsl/active_record_identity_cache.rb +213 -0
  13. data/lib/tapioca/compilers/dsl/active_record_scope.rb +100 -0
  14. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +170 -0
  15. data/lib/tapioca/compilers/dsl/active_resource.rb +140 -0
  16. data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +126 -0
  17. data/lib/tapioca/compilers/dsl/base.rb +163 -0
  18. data/lib/tapioca/compilers/dsl/frozen_record.rb +96 -0
  19. data/lib/tapioca/compilers/dsl/protobuf.rb +144 -0
  20. data/lib/tapioca/compilers/dsl/smart_properties.rb +173 -0
  21. data/lib/tapioca/compilers/dsl/state_machines.rb +378 -0
  22. data/lib/tapioca/compilers/dsl/url_helpers.rb +83 -0
  23. data/lib/tapioca/compilers/dsl_compiler.rb +121 -0
  24. data/lib/tapioca/compilers/requires_compiler.rb +67 -0
  25. data/lib/tapioca/compilers/sorbet.rb +34 -0
  26. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +209 -49
  27. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +3 -17
  28. data/lib/tapioca/compilers/todos_compiler.rb +32 -0
  29. data/lib/tapioca/config.rb +42 -0
  30. data/lib/tapioca/config_builder.rb +75 -0
  31. data/lib/tapioca/constant_locator.rb +1 -0
  32. data/lib/tapioca/core_ext/class.rb +23 -0
  33. data/lib/tapioca/gemfile.rb +14 -1
  34. data/lib/tapioca/generator.rb +235 -67
  35. data/lib/tapioca/loader.rb +20 -9
  36. data/lib/tapioca/sorbet_config_parser.rb +77 -0
  37. data/lib/tapioca/version.rb +1 -1
  38. metadata +35 -66
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2a3d27cc18a2cc1afcfd1f9c9cb1e833078ea06b1c4a4900e76614b2b54478b7
4
- data.tar.gz: 9b8e0b109fbdc3a7d6393a67df9f53031c8e29aa1479eb0a1fb644783998bc36
3
+ metadata.gz: 91a976cddc1429c3b08568b39d8fae5fe8d4d54a8b8b2b04deebb75f475987e8
4
+ data.tar.gz: 45074066696bd5c838ab09523980fd9c148faeb1b9f6ee800e8752e636edbd1a
5
5
  SHA512:
6
- metadata.gz: 5601255b31b7cec6eb03ec8553f03450148de320ef48ab3370f278797d7189c020d1a7a2c04b6ab5b2e509321df4df0fbb022dd9e0ae9477aceaca4a195b620a
7
- data.tar.gz: 1b3fcf4e9c997f60199e60e9206d2113e3d1292334d38db0d670ba0e3d5f4f0e8f348c88e74b8c1f8df0524bc9f40c59c1f214273b76259db5d88246610ccc55
6
+ metadata.gz: 1646cf3ec4957442bf0895a636bfc684e00f0d88e2252502d119fd3f4b4073e41b773ce0cbe8f725766d0c82bae7f67b92381a32a123b6fa2067ddd6a9e2c8d2
7
+ data.tar.gz: 33f8dae8437b44c2bb2e7d4a1bb483adc11c0ae58322b62fa97a879698ba3d9abab4f198d38b7a8433e57f32f64df5b39ce1b5d8a13ebe37b011dae2d179d926
data/Gemfile CHANGED
@@ -4,6 +4,32 @@ source("https://rubygems.org")
4
4
 
5
5
  gemspec
6
6
 
7
+ gem 'rubocop-shopify', require: false
8
+
7
9
  group(:deployment, :development) do
8
- gem("rake", "~> 12.3")
10
+ gem("rake")
11
+ end
12
+
13
+ gem("bundler", "~> 1.17")
14
+ gem("pry-byebug")
15
+ gem("minitest")
16
+ gem("minitest-hooks")
17
+ gem("minitest-fork_executor")
18
+ gem("minitest-reporters")
19
+ gem("sorbet")
20
+
21
+ group(:development, :test) do
22
+ gem("smart_properties", ">= 1.15.0", require: false)
23
+ gem("frozen_record", ">= 0.17", require: false)
24
+ gem("sprockets", "~> 3.7", require: false)
25
+ gem("rails", "~> 5.2", require: false)
26
+ gem("state_machines", "~> 0.5.0", require: false)
27
+ gem("activerecord-typedstore", "~> 1.3", require: false)
28
+ gem("sqlite3")
29
+ gem("identity_cache", "~> 1.0", require: false)
30
+ gem('cityhash', git: 'https://github.com/csfrancis/cityhash.git',
31
+ ref: '3cfc7d01f333c01811d5e834f1495eaa29f87c36', require: false)
32
+ gem("activemodel-serializers-xml", "~> 1.0", require: false)
33
+ gem("activeresource", "~> 5.1", require: false)
34
+ gem("google-protobuf", "~>3.12.0", require: false)
9
35
  end
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![Build Status](https://travis-ci.org/Shopify/tapioca.svg?branch=master)](https://travis-ci.org/Shopify/tapioca)
4
4
 
5
- Tapioca is a library used to generate RBI (Ruby interface) files for use with [Sorbet](https://sorbet.org). RBI files provide the structure (classes, modules, methods, parameters) of the gem/library to Sorbet to assist with typechecking.
5
+ Tapioca is a library used to generate RBI (Ruby interface) files for use with [Sorbet](https://sorbet.org). RBI files provide the structure (classes, modules, methods, parameters) of the gem/library to Sorbet to assist with typechecking.
6
6
 
7
7
  As yet, no gem exports type information in a consumable format and it would be a huge effort to manually maintain such an interface file for all the gems that your codebase depends on. Thus, there is a need for an automated way to generate the appropriate RBI file for a given gem. The `tapioca` gem, developed at Shopify, is able to do exactly that to almost 99% accuracy. It can generate the definitions for all statically defined types and most of the runtime defined types exported from Ruby gems (non-Ruby gems are not handled yet).
8
8
 
@@ -27,10 +27,17 @@ from (pry):3:in `__pry__`
27
27
  => BetterHtml::Parser
28
28
  ```
29
29
 
30
- In order to make sure that `tapioca` can reflect on that type, we need to add the line `require "better_html/parser"` to the `sorbet/tapioca/require.rb` file. This will make sure `BetterHtml::Parser` is loaded into memory and a type annotation is generated for it in the `better_html.rbi` file. If this extra `require` line is not added to `sorbet/tapioca/require.rb` file, then the definition for that type will be missing from the RBI file.
30
+ In order to make sure that `tapioca` can reflect on that type, we need to add the line `require "better_html/parser"` to the `sorbet/tapioca/require.rb` file. This will make sure `BetterHtml::Parser` is loaded into memory and a type annotation is generated for it in the `better_html.rbi` file. If this extra `require` line is not added to `sorbet/tapioca/require.rb` file, then the definition for that type will be missing from the RBI file.
31
31
 
32
32
  If you ever run into a case, where you add a gem or update the version of a gem and run `tapioca sync` but don't have some types you expect in the generated gem RBI files, you will need to make sure you have added the necessary requires to the `sorbet/tapioca/require.rb` file.
33
33
 
34
+ You can use the command `tapioca require` to auto-populate the `sorbet/tapioca/require.rb` file with all the requires found
35
+ in your application. Once the file generated, you should review it, remove all unnecessary requires and commit it.
36
+
37
+ ## How does tapioca compare to "srb rbi gems" ?
38
+
39
+ [Please see the detailed answer on our wiki](https://github.com/Shopify/tapioca/wiki/How-does-tapioca-compare-to-%22srb-rbi-gems%22-%3F)
40
+
34
41
  ## Installation
35
42
 
36
43
  Add this line to your application's `Gemfile`:
@@ -49,7 +56,9 @@ Commands:
49
56
  tapioca generate [gem...] # generate RBIs from gems
50
57
  tapioca help [COMMAND] # Describe available commands or one specific command
51
58
  tapioca init # initializes folder structure
59
+ tapioca require # generate the list of files to be required by tapioca
52
60
  tapioca sync # sync RBIs to Gemfile
61
+ tapioca todo # generate the list of unresolved constants
53
62
 
54
63
  Options:
55
64
  --pre, -b, [--prerequire=file] # A file to be required before Bundler.require is called
@@ -80,6 +89,12 @@ Command: `tapioca sync`
80
89
 
81
90
  This will sync the RBIs with the gems in the Gemfile and will add, update, and remove RBIs as necessary.
82
91
 
92
+ ### Generate the list of all unresolved constants
93
+
94
+ Command: `tapioca todo`
95
+
96
+ This will generate the file `sorbet/rbi/todo.rbi` defining all unresolved constants as empty modules.
97
+
83
98
  ### Flags
84
99
 
85
100
  - `--prerequire [file]`: A file to be required before `Bundler.require` is called.
@@ -88,6 +103,10 @@ This will sync the RBIs with the gems in the Gemfile and will add, update, and r
88
103
  - `--generate-command [command]`: The command to run to regenerate RBI files (used in header comment of the RBI files), defaults to the current command.
89
104
  - `--typed-overrides [gem:level]`: Overrides typed sigils for generated gem RBIs for gem `gem` to level `level` (`level` can be one of `ignore`, `false`, `true`, `strict`, or `strong`, see [the Sorbet docs](https://sorbet.org/docs/static#file-level-granularity-strictness-levels) for more details).
90
105
 
106
+ ### Strong typing option for ActiveRecord column methods
107
+
108
+ `tapioca` gives you the option to generate stricter type signatures for your ActiveRecord column types. By default, methods generated for columns that are defined in the schema have signatures of T.untyped. However, if the object extends a module with name StrongTypeGeneration, tapioca will generate stricter signatures that follow closely with the types defined in the schema. Expectation is the StrongTypeGeneration module you define in your application won't allow objects to be initialized with "bad state". It will check all the attributes that are not nillable to ensure they are not nil.
109
+
91
110
  ## Contributing
92
111
 
93
112
  Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/tapioca. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](https://github.com/Shopify/tapioca/blob/master/CODE_OF_CONDUCT.md) code of conduct.
data/Rakefile CHANGED
@@ -3,10 +3,21 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/testtask"
5
5
 
6
- begin
7
- require 'rspec/core/rake_task'
8
- RSpec::Core::RakeTask.new(:spec)
9
- rescue LoadError # rubocop:disable Lint/SuppressedException
6
+ Rake.application.options.trace = false
7
+
8
+ Rake::TestTask.new do |t|
9
+ t.libs << "lib"
10
+ t.libs << "spec"
11
+ t.warning = false
12
+ t.test_files = FileList['spec/**/*_spec.rb']
13
+ end
14
+
15
+ task(:spec) do
16
+ begin
17
+ Rake::Task[:test].execute
18
+ rescue RuntimeError
19
+ exit(1)
20
+ end
10
21
  end
11
22
 
12
23
  task(default: :spec)
@@ -28,12 +28,18 @@ rescue
28
28
  nil
29
29
  end
30
30
 
31
- require_relative "tapioca/loader"
32
- require_relative "tapioca/constant_locator"
33
- require_relative "tapioca/generator"
34
- require_relative "tapioca/cli"
35
- require_relative "tapioca/gemfile"
36
- require_relative "tapioca/compilers/symbol_table_compiler"
37
- require_relative "tapioca/compilers/symbol_table/symbol_generator"
38
- require_relative "tapioca/compilers/symbol_table/symbol_loader"
39
- require_relative "tapioca/version"
31
+ require "tapioca/loader"
32
+ require "tapioca/constant_locator"
33
+ require "tapioca/config"
34
+ require "tapioca/config_builder"
35
+ require "tapioca/generator"
36
+ require "tapioca/cli"
37
+ require "tapioca/gemfile"
38
+ require "tapioca/compilers/sorbet"
39
+ require "tapioca/compilers/requires_compiler"
40
+ require "tapioca/compilers/symbol_table_compiler"
41
+ require "tapioca/compilers/symbol_table/symbol_generator"
42
+ require "tapioca/compilers/symbol_table/symbol_loader"
43
+ require "tapioca/compilers/todos_compiler"
44
+ require "tapioca/compilers/dsl_compiler"
45
+ require "tapioca/version"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- # typed: false
2
+ # typed: true
3
3
 
4
4
  require 'thor'
5
5
 
@@ -13,34 +13,36 @@ module Tapioca
13
13
  desc: "A file to be required before Bundler.require is called"
14
14
  class_option :postrequire,
15
15
  aliases: ["--post", "-a"],
16
- default: Generator::DEFAULT_POSTREQUIRE,
17
16
  banner: "file",
18
17
  desc: "A file to be required after Bundler.require is called"
19
18
  class_option :outdir,
20
19
  aliases: ["--out", "-o"],
21
- default: Generator::DEFAULT_OUTDIR,
22
20
  banner: "directory",
23
21
  desc: "The output directory for generated RBI files"
24
22
  class_option :generate_command,
25
23
  aliases: ["--cmd", "-c"],
26
24
  banner: "command",
27
25
  desc: "The command to run to regenerate RBI files"
26
+ class_option :exclude,
27
+ aliases: ["-x"],
28
+ type: :array,
29
+ banner: "gem [gem ...]",
30
+ desc: "Excludes the given gem(s) from RBI generation"
28
31
  class_option :typed_overrides,
29
32
  aliases: ["--typed", "-t"],
30
33
  type: :hash,
31
- default: {},
32
- banner: "gem:level",
34
+ banner: "gem:level [gem:level ...]",
33
35
  desc: "Overrides for typed sigils for generated gem RBIs"
34
36
 
35
37
  desc "init", "initializes folder structure"
36
38
  def init
37
- create_file(Generator::SORBET_CONFIG, skip: true) do
39
+ create_file(Config::SORBET_CONFIG, skip: true) do
38
40
  <<~CONTENT
39
41
  --dir
40
42
  .
41
43
  CONTENT
42
44
  end
43
- create_file(Generator::DEFAULT_POSTREQUIRE, skip: true) do
45
+ create_file(Config::DEFAULT_POSTREQUIRE, skip: true) do
44
46
  <<~CONTENT
45
47
  # frozen_string_literal: true
46
48
  # typed: false
@@ -50,6 +52,32 @@ module Tapioca
50
52
  end
51
53
  end
52
54
 
55
+ desc "require", "generate the list of files to be required by tapioca"
56
+ def require
57
+ Tapioca.silence_warnings do
58
+ generator.build_requires
59
+ end
60
+ end
61
+
62
+ desc "todo", "generate the list of unresolved constants"
63
+ def todo
64
+ Tapioca.silence_warnings do
65
+ generator.build_todos
66
+ end
67
+ end
68
+
69
+ desc "dsl [constant...]", "generate RBIs for dynamic methods"
70
+ option :generators,
71
+ type: :array,
72
+ aliases: ["--gen", "-g"],
73
+ banner: "generator [generator ...]",
74
+ desc: "Only run supplied DSL generators"
75
+ def dsl(*constants)
76
+ Tapioca.silence_warnings do
77
+ generator.build_dsl(constants)
78
+ end
79
+ end
80
+
53
81
  desc "generate [gem...]", "generate RBIs from gems"
54
82
  def generate(*gems)
55
83
  Tapioca.silence_warnings do
@@ -65,13 +93,14 @@ module Tapioca
65
93
  end
66
94
 
67
95
  no_commands do
96
+ def self.exit_on_failure?
97
+ true
98
+ end
99
+
68
100
  def generator
101
+ current_command = T.must(current_command_chain.first)
69
102
  @generator ||= Generator.new(
70
- outdir: options[:outdir],
71
- prerequire: options[:prerequire],
72
- postrequire: options[:postrequire],
73
- command: options[:generate_command],
74
- typed_overrides: options[:typed_overrides]
103
+ ConfigBuilder.from_options(current_command, options)
75
104
  )
76
105
  end
77
106
  end
@@ -0,0 +1,129 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "action_controller"
8
+ rescue LoadError
9
+ return
10
+ end
11
+
12
+ module Tapioca
13
+ module Compilers
14
+ module Dsl
15
+ # `Tapioca::Compilers::Dsl::ActionControllerHelpers` decorates RBI files for all
16
+ # subclasses of `::ActionController::Base`
17
+ # to add helper methods (see https://api.rubyonrails.org/classes/ActionController/Helpers.html).
18
+ #
19
+ # For example, with the following `MyHelper` module:
20
+ #
21
+ # ~~~rb
22
+ # module MyHelper
23
+ # def greet(user)
24
+ # # ...
25
+ # end
26
+ #
27
+ # def localized_time
28
+ # # ...
29
+ # end
30
+ # end
31
+ # ~~~
32
+ #
33
+ # and the following controller:
34
+ #
35
+ # ~~~rb
36
+ # class UserController < ActionController::Base
37
+ # helper MyHelper
38
+ # helper { def age(user) "99" end }
39
+ # helper_method :current_user_name
40
+ #
41
+ # def current_user_name
42
+ # # ...
43
+ # end
44
+ # end
45
+ #
46
+ # this generator will produce an RBI file `user_controller.rbi` with the following content:
47
+ #
48
+ # ~~~rbi
49
+ # # user_controller.rbi
50
+ # # typed: strong
51
+ # class UserController
52
+ # sig { returns(UserController::HelperProxy) }
53
+ # def helpers; end
54
+ # end
55
+ #
56
+ # module UserController::HelperMethods
57
+ # include MyHelper
58
+ #
59
+ # sig { params(user: T.untyped).returns(T.untyped) }
60
+ # def age(user); end
61
+ #
62
+ # sig { returns(T.untyped) }
63
+ # def current_user_name; end
64
+ # end
65
+ #
66
+ # class UserController::HelperProxy < ::ActionView::Base
67
+ # include UserController::HelperMethods
68
+ # end
69
+ # ~~~
70
+ class ActionControllerHelpers < Base
71
+ extend T::Sig
72
+
73
+ sig do
74
+ override
75
+ .params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(::ActionController::Base))
76
+ .void
77
+ end
78
+ def decorate(root, constant)
79
+ helper_proxy_name = "#{constant}::HelperProxy"
80
+ helper_methods_name = "#{constant}::HelperMethods"
81
+ proxied_helper_methods = constant._helper_methods.map(&:to_s).map(&:to_sym)
82
+
83
+ # Create helper method module
84
+ root.create_module(helper_methods_name) do |helper_methods|
85
+ helpers_module = constant._helpers
86
+
87
+ gather_includes(helpers_module).each do |ancestor|
88
+ helper_methods.create_include(ancestor)
89
+ end
90
+
91
+ helpers_module.instance_methods(false).each do |method_name|
92
+ method = if proxied_helper_methods.include?(method_name)
93
+ constant.instance_method(method_name)
94
+ else
95
+ helpers_module.instance_method(method_name)
96
+ end
97
+ create_method_from_def(helper_methods, method)
98
+ end
99
+ end
100
+
101
+ # Create helper proxy class
102
+ root.create_class(helper_proxy_name, superclass: "::ActionView::Base") do |proxy|
103
+ proxy.create_include(helper_methods_name)
104
+ end
105
+
106
+ # Define the helpers method
107
+ root.path(constant) do |controller|
108
+ create_method(controller, 'helpers', return_type: helper_proxy_name)
109
+ end
110
+ end
111
+
112
+ sig { override.returns(T::Enumerable[Module]) }
113
+ def gather_constants
114
+ ::ActionController::Base.descendants.reject(&:abstract?)
115
+ end
116
+
117
+ private
118
+
119
+ sig { params(mod: Module).returns(T::Array[String]) }
120
+ def gather_includes(mod)
121
+ mod.ancestors
122
+ .reject { |ancestor| ancestor.is_a?(Class) || ancestor == mod || ancestor.name.nil? }
123
+ .map { |ancestor| T.must(ancestor.name) }
124
+ .reverse
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,65 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "action_mailer"
8
+ rescue LoadError
9
+ return
10
+ end
11
+
12
+ module Tapioca
13
+ module Compilers
14
+ module Dsl
15
+ # `Tapioca::Compilers::Dsl::ActionMailer` generates RBI files for subclasses of `ActionMailer::Base`
16
+ # (see https://api.rubyonrails.org/classes/ActionMailer/Base.html).
17
+ #
18
+ # For example, with the following `ActionMailer` subclass:
19
+ #
20
+ # ~~~rb
21
+ # class NotifierMailer < ActionMailer::Base
22
+ # def notify_customer(customer_id)
23
+ # # ...
24
+ # end
25
+ # end
26
+ # ~~~
27
+ #
28
+ # this generator will produce the RBI file `notifier_mailer.rbi` with the following content:
29
+ #
30
+ # ~~~rbi
31
+ # # notifier_mailer.rbi
32
+ # # typed: true
33
+ # class NotifierMailer
34
+ # sig { params(customer_id: T.untyped).returns(::ActionMailer::MessageDelivery) }
35
+ # def self.notify_customer(customer_id); end
36
+ # end
37
+ # ~~~
38
+ class ActionMailer < Base
39
+ extend T::Sig
40
+
41
+ sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(::ActionMailer::Base)).void }
42
+ def decorate(root, constant)
43
+ root.path(constant) do |k|
44
+ constant.action_methods.to_a.each do |mailer_method|
45
+ method_def = constant.instance_method(mailer_method)
46
+ parameters = compile_method_parameters_to_parlour(method_def)
47
+ create_method(
48
+ k,
49
+ mailer_method,
50
+ parameters: parameters,
51
+ return_type: '::ActionMailer::MessageDelivery',
52
+ class_method: true
53
+ )
54
+ end
55
+ end
56
+ end
57
+
58
+ sig { override.returns(T::Enumerable[Module]) }
59
+ def gather_constants
60
+ ::ActionMailer::Base.descendants.reject(&:abstract?)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end