tapioca 0.3.1 → 0.4.4

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