tapioca 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +25 -1
  3. data/README.md +12 -0
  4. data/Rakefile +15 -4
  5. data/lib/tapioca.rb +2 -0
  6. data/lib/tapioca/cli.rb +24 -2
  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/symbol_table/symbol_generator.rb +141 -24
  26. data/lib/tapioca/config.rb +11 -6
  27. data/lib/tapioca/config_builder.rb +19 -9
  28. data/lib/tapioca/constant_locator.rb +1 -0
  29. data/lib/tapioca/core_ext/class.rb +23 -0
  30. data/lib/tapioca/generator.rb +187 -21
  31. data/lib/tapioca/loader.rb +20 -9
  32. data/lib/tapioca/sorbet_config_parser.rb +77 -0
  33. data/lib/tapioca/version.rb +1 -1
  34. metadata +29 -51
@@ -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,163 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Compilers
6
+ module Dsl
7
+ class Base
8
+ extend T::Sig
9
+ extend T::Helpers
10
+
11
+ abstract!
12
+
13
+ sig { returns(T::Set[Module]) }
14
+ attr_reader :processable_constants
15
+
16
+ sig { void }
17
+ def initialize
18
+ @processable_constants = T.let(Set.new(gather_constants), T::Set[Module])
19
+ end
20
+
21
+ sig { params(constant: Module).returns(T::Boolean) }
22
+ def handles?(constant)
23
+ processable_constants.include?(constant)
24
+ end
25
+
26
+ sig do
27
+ abstract
28
+ .type_parameters(:T)
29
+ .params(
30
+ root: Parlour::RbiGenerator::Namespace,
31
+ constant: T.type_parameter(:T)
32
+ )
33
+ .void
34
+ end
35
+ def decorate(root, constant); end
36
+
37
+ sig { abstract.returns(T::Enumerable[Module]) }
38
+ def gather_constants; end
39
+
40
+ private
41
+
42
+ SPECIAL_METHOD_NAMES = T.let(
43
+ %w[! ~ +@ ** -@ * / % + - << >> & | ^ < <= => > >= == === != =~ !~ <=> [] []= `].freeze,
44
+ T::Array[String]
45
+ )
46
+
47
+ sig { params(name: String).returns(T::Boolean) }
48
+ def valid_method_name?(name)
49
+ return true if SPECIAL_METHOD_NAMES.include?(name)
50
+ !!name.match(/^[a-zA-Z_][[:word:]]*[?!=]?$/)
51
+ end
52
+
53
+ sig do
54
+ params(
55
+ namespace: Parlour::RbiGenerator::Namespace,
56
+ name: String,
57
+ options: T::Hash[T.untyped, T.untyped]
58
+ ).void
59
+ end
60
+ def create_method(namespace, name, options = {})
61
+ return unless valid_method_name?(name)
62
+ T.unsafe(namespace).create_method(name, options)
63
+ end
64
+
65
+ # Create a Parlour method inside `namespace` from its Ruby definition
66
+ sig do
67
+ params(
68
+ namespace: Parlour::RbiGenerator::Namespace,
69
+ method_def: T.any(Method, UnboundMethod),
70
+ class_method: T::Boolean
71
+ ).void
72
+ end
73
+ def create_method_from_def(namespace, method_def, class_method: false)
74
+ create_method(
75
+ namespace,
76
+ method_def.name.to_s,
77
+ parameters: compile_method_parameters_to_parlour(method_def),
78
+ return_type: compile_method_return_type_to_parlour(method_def),
79
+ class_method: class_method
80
+ )
81
+ end
82
+
83
+ # Compile a Ruby method parameters into Parlour parameters
84
+ sig do
85
+ params(method_def: T.any(Method, UnboundMethod))
86
+ .returns(T::Array[Parlour::RbiGenerator::Parameter])
87
+ end
88
+ def compile_method_parameters_to_parlour(method_def)
89
+ signature = T::Private::Methods.signature_for_method(method_def)
90
+ method_def = signature.nil? ? method_def : signature.method
91
+ method_types = parameters_types_from_signature(method_def, signature)
92
+
93
+ method_def.parameters.each_with_index.map do |(type, name), i|
94
+ name ||= :_
95
+ name = name.to_s.gsub(/&|\*/, '_') # avoid incorrect names from `delegate`
96
+ case type
97
+ when :req
98
+ ::Parlour::RbiGenerator::Parameter.new(name, type: method_types[i])
99
+ when :opt
100
+ ::Parlour::RbiGenerator::Parameter.new(name, type: method_types[i], default: 'T.unsafe(nil)')
101
+ when :rest
102
+ ::Parlour::RbiGenerator::Parameter.new("*#{name}", type: method_types[i])
103
+ when :keyreq
104
+ ::Parlour::RbiGenerator::Parameter.new("#{name}:", type: method_types[i])
105
+ when :key
106
+ ::Parlour::RbiGenerator::Parameter.new("#{name}:", type: method_types[i], default: 'T.unsafe(nil)')
107
+ when :keyrest
108
+ ::Parlour::RbiGenerator::Parameter.new("**#{name}", type: method_types[i])
109
+ when :block
110
+ ::Parlour::RbiGenerator::Parameter.new("&#{name}", type: method_types[i])
111
+ else
112
+ raise "Unknown type `#{type}`."
113
+ end
114
+ end
115
+ end
116
+
117
+ # Compile a Ruby method return type into a Parlour type
118
+ sig do
119
+ params(method_def: T.any(Method, UnboundMethod))
120
+ .returns(String)
121
+ end
122
+ def compile_method_return_type_to_parlour(method_def)
123
+ signature = T::Private::Methods.signature_for_method(method_def)
124
+ return_type = signature.nil? ? 'T.untyped' : signature.return_type.to_s
125
+ # Map <VOID> to `nil` since `nil` means a `void` return for Parlour
126
+ return_type = nil if return_type == "<VOID>"
127
+ # Map <NOT-TYPED> to `T.untyped`
128
+ return_type = "T.untyped" if return_type == "<NOT-TYPED>"
129
+ return_type
130
+ end
131
+
132
+ # Get the types of each parameter from a method signature
133
+ sig do
134
+ params(
135
+ method_def: T.any(Method, UnboundMethod),
136
+ signature: T.untyped # as `T::Private::Methods::Signature` is private
137
+ ).returns(T::Array[String])
138
+ end
139
+ def parameters_types_from_signature(method_def, signature)
140
+ params = T.let([], T::Array[String])
141
+
142
+ return method_def.parameters.map { 'T.untyped' } unless signature
143
+
144
+ # parameters types
145
+ signature.arg_types.each { |arg_type| params << arg_type[1].to_s }
146
+
147
+ # keyword parameters types
148
+ signature.kwarg_types.each { |_, kwarg_type| params << kwarg_type.to_s }
149
+
150
+ # rest parameter type
151
+ params << signature.rest_type.to_s if signature.has_rest
152
+
153
+ # special case `.void` in a proc
154
+ unless signature.block_name.nil?
155
+ params << signature.block_type.to_s.gsub('returns(<VOID>)', 'void')
156
+ end
157
+
158
+ params
159
+ end
160
+ end
161
+ end
162
+ end
163
+ 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