tapioca 0.3.1 → 0.4.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +25 -1
  3. data/README.md +23 -2
  4. data/Rakefile +15 -4
  5. data/lib/tapioca.rb +8 -2
  6. data/lib/tapioca/cli.rb +32 -3
  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 +267 -0
  10. data/lib/tapioca/compilers/dsl/active_record_columns.rb +393 -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 +165 -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 +92 -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 +171 -26
  27. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +1 -20
  28. data/lib/tapioca/compilers/todos_compiler.rb +32 -0
  29. data/lib/tapioca/config.rb +14 -6
  30. data/lib/tapioca/config_builder.rb +22 -9
  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 +32 -9
  34. data/lib/tapioca/generator.rb +231 -23
  35. data/lib/tapioca/loader.rb +30 -9
  36. data/lib/tapioca/version.rb +1 -1
  37. metadata +32 -39
@@ -0,0 +1,140 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "active_resource"
8
+ rescue LoadError
9
+ return
10
+ end
11
+
12
+ module Tapioca
13
+ module Compilers
14
+ module Dsl
15
+ # `Tapioca::Compilers::Dsl::ActiveResource` decorates RBI files for subclasses of
16
+ # `ActiveResource::Base` which declare `schema` fields
17
+ # (see https://github.com/rails/activeresource).
18
+ #
19
+ # For example, with the following `ActiveResource::Base` subclass:
20
+ #
21
+ # ~~~rb
22
+ # class Post < ActiveResource::Base
23
+ # schema do
24
+ # integer 'id', 'month', 'year'
25
+ # end
26
+ # end
27
+ # ~~~
28
+ #
29
+ # this generator will produce the RBI file `post.rbi` with the following content:
30
+ #
31
+ # ~~~rbi
32
+ # # post.rbi
33
+ # # typed: true
34
+ # class Post
35
+ # sig { returns(Integer) }
36
+ # def id; end
37
+ #
38
+ # sig { params(id: Integer).returns(Integer) }
39
+ # def id=(id); end
40
+ #
41
+ # sig { returns(T::Boolean) }
42
+ # def id?; end
43
+ #
44
+ # sig { returns(Integer) }
45
+ # def month; end
46
+ #
47
+ # sig { params(month: Integer).returns(Integer) }
48
+ # def month=(month); end
49
+ #
50
+ # sig { returns(T::Boolean) }
51
+ # def month?; end
52
+ #
53
+ # sig { returns(Integer) }
54
+ # def year; end
55
+ #
56
+ # sig { params(year: Integer).returns(Integer) }
57
+ # def year=(year); end
58
+ #
59
+ # sig { returns(T::Boolean) }
60
+ # def year?; end
61
+ # end
62
+ # ~~~
63
+ class ActiveResource < Base
64
+ extend T::Sig
65
+
66
+ sig do
67
+ override.params(
68
+ root: Parlour::RbiGenerator::Namespace,
69
+ constant: T.class_of(::ActiveResource::Base)
70
+ ).void
71
+ end
72
+ def decorate(root, constant)
73
+ return if constant.schema.blank?
74
+ root.path(constant) do |klass|
75
+ constant.schema.each do |attribute, type|
76
+ create_schema_methods(klass, attribute, type)
77
+ end
78
+ end
79
+ end
80
+
81
+ sig { override.returns(T::Enumerable[Module]) }
82
+ def gather_constants
83
+ ::ActiveResource::Base.descendants
84
+ end
85
+
86
+ private
87
+
88
+ TYPES = T.let({
89
+ boolean: "T::Boolean",
90
+ integer: "Integer",
91
+ string: "String",
92
+ float: "Float",
93
+ date: "Date",
94
+ time: "Time",
95
+ datetime: "DateTime",
96
+ decimal: "BigDecimal",
97
+ binary: "String",
98
+ text: "String",
99
+ }.freeze, T::Hash[Symbol, String])
100
+
101
+ sig { params(attr_type: Symbol).returns(String) }
102
+ def type_for(attr_type)
103
+ TYPES.fetch(attr_type, "T.untyped")
104
+ end
105
+
106
+ sig do
107
+ params(
108
+ klass: Parlour::RbiGenerator::Namespace,
109
+ attribute: String,
110
+ type: String
111
+ ).void
112
+ end
113
+ def create_schema_methods(klass, attribute, type)
114
+ return_type = type_for(type.to_sym)
115
+
116
+ create_method(
117
+ klass,
118
+ attribute,
119
+ return_type: return_type
120
+ )
121
+
122
+ create_method(
123
+ klass,
124
+ "#{attribute}?",
125
+ return_type: "T::Boolean"
126
+ )
127
+
128
+ create_method(
129
+ klass,
130
+ "#{attribute}=",
131
+ parameters: [
132
+ Parlour::RbiGenerator::Parameter.new("value", type: return_type),
133
+ ],
134
+ return_type: return_type
135
+ )
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,126 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "active_support"
8
+ rescue LoadError
9
+ return
10
+ end
11
+
12
+ module Tapioca
13
+ module Compilers
14
+ module Dsl
15
+ # `Tapioca::Compilers::Dsl::ActiveSupportCurrentAttributes` decorates RBI files for all
16
+ # subclasses of `ActiveSupport::CurrentAttributes`
17
+ #
18
+ # To add attributes see https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html
19
+ #
20
+ # For example, with the following singleton class
21
+ #
22
+ # ~~~rb
23
+ # class Current < ActiveSupport::CurrentAttributes
24
+ # extend T::Sig
25
+ #
26
+ # attribute :account
27
+ #
28
+ # def helper
29
+ # # ...
30
+ # end
31
+ #
32
+ # sig { params(user_id: Integer).void }
33
+ # def authenticate(user_id)
34
+ # # ...
35
+ # end
36
+ # end
37
+ # ~~~rb
38
+ #
39
+ # this generator will produce an RBI file with the following content:
40
+ # ~~~rbi
41
+ # # typed: true
42
+ #
43
+ # class Current
44
+ # sig { returns(T.untyped) }
45
+ # def self.account; end
46
+ #
47
+ # sig { returns(T.untyped) }
48
+ # def account; end
49
+ #
50
+ # sig { params(account: T.untyped).returns(T.untyped) }
51
+ # def self.account=(account); end
52
+ #
53
+ # sig { params(account: T.untyped).returns(T.untyped) }
54
+ # def account=(account); end
55
+ #
56
+ # sig { params(user_id: Integer).void }
57
+ # def self.authenticate(user_id); end
58
+ #
59
+ # sig { returns(T.untyped) }
60
+ # def self.helper; end
61
+ # end
62
+ # ~~~
63
+ class ActiveSupportCurrentAttributes < Base
64
+ extend T::Sig
65
+
66
+ sig do
67
+ override
68
+ .params(
69
+ root: Parlour::RbiGenerator::Namespace,
70
+ constant: T.class_of(::ActiveSupport::CurrentAttributes)
71
+ )
72
+ .void
73
+ end
74
+ def decorate(root, constant)
75
+ dynamic_methods = dynamic_methods_for(constant)
76
+ instance_methods = instance_methods_for(constant) - dynamic_methods
77
+ return if dynamic_methods.empty? && instance_methods.empty?
78
+
79
+ root.path(constant) do |k|
80
+ dynamic_methods.each do |method|
81
+ method = method.to_s
82
+ # We want to generate each method both on the class
83
+ generate_method(k, method, class_method: true)
84
+ # and on the instance
85
+ generate_method(k, method, class_method: false)
86
+ end
87
+
88
+ instance_methods.each do |method|
89
+ # instance methods are only elevated to class methods
90
+ # no need to add separate instance methods for them
91
+ method = constant.instance_method(method)
92
+ create_method_from_def(k, method, class_method: true)
93
+ end
94
+ end
95
+ end
96
+
97
+ sig { override.returns(T::Enumerable[Module]) }
98
+ def gather_constants
99
+ ::ActiveSupport::CurrentAttributes.descendants
100
+ end
101
+
102
+ private
103
+
104
+ sig { params(constant: T.class_of(::ActiveSupport::CurrentAttributes)).returns(T::Array[Symbol]) }
105
+ def dynamic_methods_for(constant)
106
+ constant.instance_variable_get(:@generated_attribute_methods)&.instance_methods(false) || []
107
+ end
108
+
109
+ sig { params(constant: T.class_of(::ActiveSupport::CurrentAttributes)).returns(T::Array[Symbol]) }
110
+ def instance_methods_for(constant)
111
+ constant.instance_methods(false)
112
+ end
113
+
114
+ sig { params(klass: Parlour::RbiGenerator::Namespace, method: String, class_method: T::Boolean).void }
115
+ def generate_method(klass, method, class_method:)
116
+ if method.end_with?("=")
117
+ parameter = Parlour::RbiGenerator::Parameter.new("value", type: "T.untyped")
118
+ klass.create_method(method, class_method: class_method, parameters: [parameter], return_type: "T.untyped")
119
+ else
120
+ klass.create_method(method, class_method: class_method, return_type: "T.untyped")
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,165 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ module Tapioca
7
+ module Compilers
8
+ module Dsl
9
+ class Base
10
+ extend T::Sig
11
+ extend T::Helpers
12
+
13
+ abstract!
14
+
15
+ sig { returns(T::Set[Module]) }
16
+ attr_reader :processable_constants
17
+
18
+ sig { void }
19
+ def initialize
20
+ @processable_constants = T.let(Set.new(gather_constants), T::Set[Module])
21
+ end
22
+
23
+ sig { params(constant: Module).returns(T::Boolean) }
24
+ def handles?(constant)
25
+ processable_constants.include?(constant)
26
+ end
27
+
28
+ sig do
29
+ abstract
30
+ .type_parameters(:T)
31
+ .params(
32
+ root: Parlour::RbiGenerator::Namespace,
33
+ constant: T.type_parameter(:T)
34
+ )
35
+ .void
36
+ end
37
+ def decorate(root, constant); end
38
+
39
+ sig { abstract.returns(T::Enumerable[Module]) }
40
+ def gather_constants; end
41
+
42
+ private
43
+
44
+ SPECIAL_METHOD_NAMES = T.let(
45
+ %w[! ~ +@ ** -@ * / % + - << >> & | ^ < <= => > >= == === != =~ !~ <=> [] []= `].freeze,
46
+ T::Array[String]
47
+ )
48
+
49
+ sig { params(name: String).returns(T::Boolean) }
50
+ def valid_method_name?(name)
51
+ return true if SPECIAL_METHOD_NAMES.include?(name)
52
+ !!name.match(/^[a-zA-Z_][[:word:]]*[?!=]?$/)
53
+ end
54
+
55
+ sig do
56
+ params(
57
+ namespace: Parlour::RbiGenerator::Namespace,
58
+ name: String,
59
+ options: T::Hash[T.untyped, T.untyped]
60
+ ).void
61
+ end
62
+ def create_method(namespace, name, options = {})
63
+ return unless valid_method_name?(name)
64
+ T.unsafe(namespace).create_method(name, options)
65
+ end
66
+
67
+ # Create a Parlour method inside `namespace` from its Ruby definition
68
+ sig do
69
+ params(
70
+ namespace: Parlour::RbiGenerator::Namespace,
71
+ method_def: T.any(Method, UnboundMethod),
72
+ class_method: T::Boolean
73
+ ).void
74
+ end
75
+ def create_method_from_def(namespace, method_def, class_method: false)
76
+ create_method(
77
+ namespace,
78
+ method_def.name.to_s,
79
+ parameters: compile_method_parameters_to_parlour(method_def),
80
+ return_type: compile_method_return_type_to_parlour(method_def),
81
+ class_method: class_method
82
+ )
83
+ end
84
+
85
+ # Compile a Ruby method parameters into Parlour parameters
86
+ sig do
87
+ params(method_def: T.any(Method, UnboundMethod))
88
+ .returns(T::Array[Parlour::RbiGenerator::Parameter])
89
+ end
90
+ def compile_method_parameters_to_parlour(method_def)
91
+ signature = T::Private::Methods.signature_for_method(method_def)
92
+ method_def = signature.nil? ? method_def : signature.method
93
+ method_types = parameters_types_from_signature(method_def, signature)
94
+
95
+ method_def.parameters.each_with_index.map do |(type, name), i|
96
+ name ||= :_
97
+ name = name.to_s.gsub(/&|\*/, '_') # avoid incorrect names from `delegate`
98
+ case type
99
+ when :req
100
+ ::Parlour::RbiGenerator::Parameter.new(name, type: method_types[i])
101
+ when :opt
102
+ ::Parlour::RbiGenerator::Parameter.new(name, type: method_types[i], default: 'T.unsafe(nil)')
103
+ when :rest
104
+ ::Parlour::RbiGenerator::Parameter.new("*#{name}", type: method_types[i])
105
+ when :keyreq
106
+ ::Parlour::RbiGenerator::Parameter.new("#{name}:", type: method_types[i])
107
+ when :key
108
+ ::Parlour::RbiGenerator::Parameter.new("#{name}:", type: method_types[i], default: 'T.unsafe(nil)')
109
+ when :keyrest
110
+ ::Parlour::RbiGenerator::Parameter.new("**#{name}", type: method_types[i])
111
+ when :block
112
+ ::Parlour::RbiGenerator::Parameter.new("&#{name}", type: method_types[i])
113
+ else
114
+ raise "Unknown type `#{type}`."
115
+ end
116
+ end
117
+ end
118
+
119
+ # Compile a Ruby method return type into a Parlour type
120
+ sig do
121
+ params(method_def: T.any(Method, UnboundMethod))
122
+ .returns(String)
123
+ end
124
+ def compile_method_return_type_to_parlour(method_def)
125
+ signature = T::Private::Methods.signature_for_method(method_def)
126
+ return_type = signature.nil? ? 'T.untyped' : signature.return_type.to_s
127
+ # Map <VOID> to `nil` since `nil` means a `void` return for Parlour
128
+ return_type = nil if return_type == "<VOID>"
129
+ # Map <NOT-TYPED> to `T.untyped`
130
+ return_type = "T.untyped" if return_type == "<NOT-TYPED>"
131
+ return_type
132
+ end
133
+
134
+ # Get the types of each parameter from a method signature
135
+ sig do
136
+ params(
137
+ method_def: T.any(Method, UnboundMethod),
138
+ signature: T.untyped # as `T::Private::Methods::Signature` is private
139
+ ).returns(T::Array[String])
140
+ end
141
+ def parameters_types_from_signature(method_def, signature)
142
+ params = T.let([], T::Array[String])
143
+
144
+ return method_def.parameters.map { 'T.untyped' } unless signature
145
+
146
+ # parameters types
147
+ signature.arg_types.each { |arg_type| params << arg_type[1].to_s }
148
+
149
+ # keyword parameters types
150
+ signature.kwarg_types.each { |_, kwarg_type| params << kwarg_type.to_s }
151
+
152
+ # rest parameter type
153
+ params << signature.rest_type.to_s if signature.has_rest
154
+
155
+ # special case `.void` in a proc
156
+ unless signature.block_name.nil?
157
+ params << signature.block_type.to_s.gsub('returns(<VOID>)', 'void')
158
+ end
159
+
160
+ params
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,96 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "parlour"
5
+
6
+ begin
7
+ require "frozen_record"
8
+ rescue LoadError
9
+ return
10
+ end
11
+
12
+ module Tapioca
13
+ module Compilers
14
+ module Dsl
15
+ # `Tapioca::Compilers::Dsl::FrozenRecord` generates RBI files for subclasses of `FrozenRecord::Base`
16
+ # (see https://github.com/byroot/frozen_record).
17
+ #
18
+ # For example, with the following FrozenRecord class:
19
+ #
20
+ # ~~~rb
21
+ # # student.rb
22
+ # class Student < FrozenRecord::Base
23
+ # end
24
+ # ~~~
25
+ #
26
+ # and the following YAML file:
27
+ #
28
+ # ~~~ yaml
29
+ # # students.yml
30
+ # - id: 1
31
+ # first_name: John
32
+ # last_name: Smith
33
+ # - id: 2
34
+ # first_name: Dan
35
+ # last_name: Lord
36
+ # ~~~
37
+ #
38
+ # this generator will produce the RBI file `student.rbi` with the following content:
39
+ #
40
+ # ~~~rbi
41
+ # # Student.rbi
42
+ # # typed: strong
43
+ # class Student
44
+ # include Student::FrozenRecordAttributeMethods
45
+ # end
46
+ #
47
+ # module Student::FrozenRecordAttributeMethods
48
+ # sig { returns(T.untyped) }
49
+ # def first_name; end
50
+ #
51
+ # sig { returns(T::Boolean) }
52
+ # def first_name?; end
53
+ #
54
+ # sig { returns(T.untyped) }
55
+ # def id; end
56
+ #
57
+ # sig { returns(T::Boolean) }
58
+ # def id?; end
59
+ #
60
+ # sig { returns(T.untyped) }
61
+ # def last_name; end
62
+ #
63
+ # sig { returns(T::Boolean) }
64
+ # def last_name?; end
65
+ # end
66
+ # ~~~
67
+ class FrozenRecord < Base
68
+ extend T::Sig
69
+
70
+ sig { override.params(root: Parlour::RbiGenerator::Namespace, constant: T.class_of(::FrozenRecord::Base)).void }
71
+ def decorate(root, constant)
72
+ attributes = constant.attributes
73
+ return if attributes.empty?
74
+
75
+ module_name = "#{constant}::FrozenRecordAttributeMethods"
76
+
77
+ root.create_module(module_name) do |mod|
78
+ attributes.each do |attribute|
79
+ create_method(mod, "#{attribute}?", return_type: 'T::Boolean')
80
+ create_method(mod, attribute.to_s, return_type: 'T.untyped')
81
+ end
82
+ end
83
+
84
+ root.path(constant) do |klass|
85
+ klass.create_include(module_name)
86
+ end
87
+ end
88
+
89
+ sig { override.returns(T::Enumerable[Module]) }
90
+ def gather_constants
91
+ ::FrozenRecord::Base.descendants.reject(&:abstract_class?)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end