tapioca 0.4.27 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +14 -14
  3. data/README.md +2 -2
  4. data/Rakefile +5 -7
  5. data/exe/tapioca +2 -2
  6. data/lib/tapioca/cli.rb +256 -2
  7. data/lib/tapioca/compilers/dsl/aasm.rb +122 -0
  8. data/lib/tapioca/compilers/dsl/action_controller_helpers.rb +52 -12
  9. data/lib/tapioca/compilers/dsl/action_mailer.rb +6 -9
  10. data/lib/tapioca/compilers/dsl/active_job.rb +8 -12
  11. data/lib/tapioca/compilers/dsl/active_model_attributes.rb +131 -0
  12. data/lib/tapioca/compilers/dsl/active_record_associations.rb +33 -54
  13. data/lib/tapioca/compilers/dsl/active_record_columns.rb +10 -105
  14. data/lib/tapioca/compilers/dsl/active_record_enum.rb +8 -10
  15. data/lib/tapioca/compilers/dsl/active_record_scope.rb +7 -10
  16. data/lib/tapioca/compilers/dsl/active_record_typed_store.rb +5 -8
  17. data/lib/tapioca/compilers/dsl/active_resource.rb +9 -37
  18. data/lib/tapioca/compilers/dsl/active_storage.rb +98 -0
  19. data/lib/tapioca/compilers/dsl/active_support_concern.rb +108 -0
  20. data/lib/tapioca/compilers/dsl/active_support_current_attributes.rb +13 -8
  21. data/lib/tapioca/compilers/dsl/base.rb +96 -82
  22. data/lib/tapioca/compilers/dsl/config.rb +111 -0
  23. data/lib/tapioca/compilers/dsl/frozen_record.rb +5 -7
  24. data/lib/tapioca/compilers/dsl/identity_cache.rb +66 -29
  25. data/lib/tapioca/compilers/dsl/protobuf.rb +19 -69
  26. data/lib/tapioca/compilers/dsl/sidekiq_worker.rb +25 -12
  27. data/lib/tapioca/compilers/dsl/smart_properties.rb +19 -31
  28. data/lib/tapioca/compilers/dsl/state_machines.rb +56 -78
  29. data/lib/tapioca/compilers/dsl/url_helpers.rb +7 -10
  30. data/lib/tapioca/compilers/dsl_compiler.rb +22 -38
  31. data/lib/tapioca/compilers/requires_compiler.rb +2 -2
  32. data/lib/tapioca/compilers/sorbet.rb +26 -5
  33. data/lib/tapioca/compilers/symbol_table/symbol_generator.rb +138 -153
  34. data/lib/tapioca/compilers/symbol_table/symbol_loader.rb +4 -4
  35. data/lib/tapioca/compilers/todos_compiler.rb +1 -1
  36. data/lib/tapioca/config.rb +2 -0
  37. data/lib/tapioca/config_builder.rb +4 -2
  38. data/lib/tapioca/constant_locator.rb +6 -8
  39. data/lib/tapioca/gemfile.rb +2 -4
  40. data/lib/tapioca/generator.rb +124 -40
  41. data/lib/tapioca/generic_type_registry.rb +25 -98
  42. data/lib/tapioca/helpers/active_record_column_type_helper.rb +98 -0
  43. data/lib/tapioca/internal.rb +2 -9
  44. data/lib/tapioca/loader.rb +13 -33
  45. data/lib/tapioca/rbi_ext/model.rb +122 -0
  46. data/lib/tapioca/reflection.rb +131 -0
  47. data/lib/tapioca/sorbet_ext/fixed_hash_patch.rb +1 -1
  48. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +72 -4
  49. data/lib/tapioca/sorbet_ext/name_patch.rb +1 -1
  50. data/lib/tapioca/version.rb +1 -1
  51. data/lib/tapioca.rb +2 -1
  52. metadata +34 -22
  53. data/lib/tapioca/cli/main.rb +0 -146
  54. data/lib/tapioca/core_ext/class.rb +0 -28
  55. data/lib/tapioca/core_ext/string.rb +0 -18
  56. data/lib/tapioca/rbi/model.rb +0 -405
  57. data/lib/tapioca/rbi/printer.rb +0 -410
  58. data/lib/tapioca/rbi/rewriters/group_nodes.rb +0 -106
  59. data/lib/tapioca/rbi/rewriters/nest_non_public_methods.rb +0 -65
  60. data/lib/tapioca/rbi/rewriters/nest_singleton_methods.rb +0 -42
  61. data/lib/tapioca/rbi/rewriters/sort_nodes.rb +0 -86
  62. data/lib/tapioca/rbi/visitor.rb +0 -21
@@ -0,0 +1,98 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ class ActiveRecordColumnTypeHelper
5
+ extend T::Sig
6
+
7
+ sig { params(constant: T.class_of(ActiveRecord::Base)).void }
8
+ def initialize(constant)
9
+ @constant = constant
10
+ end
11
+
12
+ sig { params(column_name: String).returns([String, String]) }
13
+ def type_for(column_name)
14
+ return ["T.untyped", "T.untyped"] if do_not_generate_strong_types?(@constant)
15
+
16
+ column_type = @constant.attribute_types[column_name]
17
+
18
+ getter_type =
19
+ case column_type
20
+ when defined?(MoneyColumn) && MoneyColumn::ActiveRecordType
21
+ "::Money"
22
+ when ActiveRecord::Type::Integer
23
+ "::Integer"
24
+ when ActiveRecord::Type::String
25
+ "::String"
26
+ when ActiveRecord::Type::Date
27
+ "::Date"
28
+ when ActiveRecord::Type::Decimal
29
+ "::BigDecimal"
30
+ when ActiveRecord::Type::Float
31
+ "::Float"
32
+ when ActiveRecord::Type::Boolean
33
+ "T::Boolean"
34
+ when ActiveRecord::Type::DateTime, ActiveRecord::Type::Time
35
+ "::DateTime"
36
+ when ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter
37
+ "::ActiveSupport::TimeWithZone"
38
+ else
39
+ handle_unknown_type(column_type)
40
+ end
41
+
42
+ column = @constant.columns_hash[column_name]
43
+ setter_type = getter_type
44
+
45
+ if column&.null
46
+ return ["T.nilable(#{getter_type})", "T.nilable(#{setter_type})"]
47
+ end
48
+
49
+ if column_name == @constant.primary_key ||
50
+ column_name == "created_at" ||
51
+ column_name == "updated_at"
52
+ getter_type = "T.nilable(#{getter_type})"
53
+ end
54
+
55
+ [getter_type, setter_type]
56
+ end
57
+
58
+ private
59
+
60
+ sig { params(constant: Module).returns(T::Boolean) }
61
+ def do_not_generate_strong_types?(constant)
62
+ Object.const_defined?(:StrongTypeGeneration) &&
63
+ !(constant.singleton_class < Object.const_get(:StrongTypeGeneration))
64
+ end
65
+
66
+ sig { params(column_type: Object).returns(String) }
67
+ def handle_unknown_type(column_type)
68
+ return "T.untyped" unless ActiveModel::Type::Value === column_type
69
+
70
+ lookup_return_type_of_method(column_type, :deserialize) ||
71
+ lookup_return_type_of_method(column_type, :cast) ||
72
+ lookup_arg_type_of_method(column_type, :serialize) ||
73
+ "T.untyped"
74
+ end
75
+
76
+ sig { params(column_type: ActiveModel::Type::Value, method: Symbol).returns(T.nilable(String)) }
77
+ def lookup_return_type_of_method(column_type, method)
78
+ signature = T::Private::Methods.signature_for_method(column_type.method(method))
79
+ return unless signature
80
+
81
+ return_type = signature.return_type
82
+ return if return_type == T::Private::Types::Void || return_type == T::Private::Types::NotTyped
83
+
84
+ return_type.to_s
85
+ end
86
+
87
+ sig { params(column_type: ActiveModel::Type::Value, method: Symbol).returns(T.nilable(String)) }
88
+ def lookup_arg_type_of_method(column_type, method)
89
+ signature = T::Private::Methods.signature_for_method(column_type.method(method))
90
+ return unless signature
91
+
92
+ # Arg types is an array [name, type] entries, so we desctructure the type of
93
+ # first argument to get the first argument type
94
+ _, first_argument_type = signature.arg_types.first
95
+
96
+ first_argument_type.to_s
97
+ end
98
+ end
@@ -3,22 +3,15 @@
3
3
 
4
4
  require "tapioca"
5
5
  require "tapioca/loader"
6
- require "tapioca/generic_type_registry"
6
+ require "tapioca/constant_locator"
7
7
  require "tapioca/sorbet_ext/generic_name_patch"
8
8
  require "tapioca/sorbet_ext/fixed_hash_patch"
9
+ require "tapioca/generic_type_registry"
9
10
  require "tapioca/config"
10
11
  require "tapioca/config_builder"
11
12
  require "tapioca/generator"
12
13
  require "tapioca/cli"
13
- require "tapioca/cli/main"
14
14
  require "tapioca/gemfile"
15
- require "tapioca/rbi/model"
16
- require "tapioca/rbi/visitor"
17
- require "tapioca/rbi/rewriters/nest_singleton_methods"
18
- require "tapioca/rbi/rewriters/nest_non_public_methods"
19
- require "tapioca/rbi/rewriters/group_nodes"
20
- require "tapioca/rbi/rewriters/sort_nodes"
21
- require "tapioca/rbi/printer"
22
15
  require "tapioca/compilers/sorbet"
23
16
  require "tapioca/compilers/requires_compiler"
24
17
  require "tapioca/compilers/symbol_table_compiler"
@@ -5,19 +5,14 @@ module Tapioca
5
5
  class Loader
6
6
  extend(T::Sig)
7
7
 
8
- sig { params(gemfile: Tapioca::Gemfile).void }
9
- def initialize(gemfile)
10
- @gemfile = T.let(gemfile, Tapioca::Gemfile)
11
- end
12
-
13
- sig { params(initialize_file: T.nilable(String), require_file: T.nilable(String)).void }
14
- def load_bundle(initialize_file, require_file)
8
+ sig { params(gemfile: Tapioca::Gemfile, initialize_file: T.nilable(String), require_file: T.nilable(String)).void }
9
+ def load_bundle(gemfile, initialize_file, require_file)
15
10
  require_helper(initialize_file)
16
11
 
17
- load_rails_application
18
-
19
12
  gemfile.require_bundle
20
13
 
14
+ load_rails_application
15
+
21
16
  require_helper(require_file)
22
17
 
23
18
  load_rails_engines
@@ -40,9 +35,6 @@ module Tapioca
40
35
 
41
36
  private
42
37
 
43
- sig { returns(Tapioca::Gemfile) }
44
- attr_reader :gemfile
45
-
46
38
  sig { params(file: T.nilable(String)).void }
47
39
  def require_helper(file)
48
40
  return unless file
@@ -54,18 +46,10 @@ module Tapioca
54
46
 
55
47
  sig { returns(T::Array[T.untyped]) }
56
48
  def rails_engines
57
- engines = []
58
-
59
- return engines unless Object.const_defined?("Rails::Engine")
49
+ return [] unless Object.const_defined?("Rails::Engine")
60
50
 
61
- base = Object.const_get("Rails::Engine")
62
- ObjectSpace.each_object(base.singleton_class) do |k|
63
- k = T.cast(k, Class)
64
- next if k.singleton_class?
65
- engines.unshift(k) unless k == base
66
- end
67
-
68
- engines.reject(&:abstract_railtie?)
51
+ # We can use `Class#descendants` here, since we know Rails is loaded
52
+ Object.const_get("Rails::Engine").descendants.reject(&:abstract_railtie?)
69
53
  end
70
54
 
71
55
  sig { params(path: String).void }
@@ -116,22 +100,18 @@ module Tapioca
116
100
 
117
101
  engine.config.eager_load_paths.each do |load_path|
118
102
  Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
119
- begin
120
- require(file)
121
- rescue LoadError, StandardError
122
- errored_files << file
123
- end
103
+ require(file)
104
+ rescue LoadError, StandardError
105
+ errored_files << file
124
106
  end
125
107
  end
126
108
 
127
109
  # Try files that have errored one more time
128
110
  # It might have been a load order problem
129
111
  errored_files.each do |file|
130
- begin
131
- require(file)
132
- rescue LoadError, StandardError
133
- nil
134
- end
112
+ require(file)
113
+ rescue LoadError, StandardError
114
+ nil
135
115
  end
136
116
  end
137
117
  end
@@ -0,0 +1,122 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "rbi"
5
+
6
+ module RBI
7
+ class Tree
8
+ extend T::Sig
9
+
10
+ sig { params(constant: ::Module, block: T.nilable(T.proc.params(scope: Scope).void)).void }
11
+ def create_path(constant, &block)
12
+ constant_name = Tapioca::Reflection.name_of(constant)
13
+ raise "given constant does not have a name" unless constant_name
14
+
15
+ instance = ::Module.const_get(constant_name)
16
+ case instance
17
+ when ::Class
18
+ create_class(constant.to_s, &block)
19
+ when ::Module
20
+ create_module(constant.to_s, &block)
21
+ else
22
+ raise "unexpected type: #{constant_name} is a #{instance.class}"
23
+ end
24
+ end
25
+
26
+ sig { params(name: String, block: T.nilable(T.proc.params(scope: Scope).void)).void }
27
+ def create_module(name, &block)
28
+ node = create_node(RBI::Module.new(name))
29
+ block&.call(T.cast(node, RBI::Scope))
30
+ end
31
+
32
+ sig do
33
+ params(
34
+ name: String,
35
+ superclass_name: T.nilable(String),
36
+ block: T.nilable(T.proc.params(scope: RBI::Scope).void)
37
+ ).void
38
+ end
39
+ def create_class(name, superclass_name: nil, &block)
40
+ node = create_node(RBI::Class.new(name, superclass_name: superclass_name))
41
+ block&.call(T.cast(node, RBI::Scope))
42
+ end
43
+
44
+ sig { params(name: String, value: String).void }
45
+ def create_constant(name, value:)
46
+ create_node(RBI::Const.new(name, value))
47
+ end
48
+
49
+ sig { params(name: String).void }
50
+ def create_include(name)
51
+ create_node(RBI::Include.new(name))
52
+ end
53
+
54
+ sig { params(name: String).void }
55
+ def create_extend(name)
56
+ create_node(RBI::Extend.new(name))
57
+ end
58
+
59
+ sig { params(name: String).void }
60
+ def create_mixes_in_class_methods(name)
61
+ create_node(RBI::MixesInClassMethods.new(name))
62
+ end
63
+
64
+ sig { params(name: String, value: String).void }
65
+ def create_type_member(name, value: "type_member")
66
+ create_node(RBI::TypeMember.new(name, value))
67
+ end
68
+
69
+ sig do
70
+ params(
71
+ name: String,
72
+ parameters: T::Array[TypedParam],
73
+ return_type: String,
74
+ class_method: T::Boolean
75
+ ).void
76
+ end
77
+ def create_method(name, parameters: [], return_type: "T.untyped", class_method: false)
78
+ return unless valid_method_name?(name)
79
+
80
+ sig = RBI::Sig.new(return_type: return_type)
81
+ method = RBI::Method.new(name, sigs: [sig], is_singleton: class_method)
82
+ parameters.each do |param|
83
+ method << param.param
84
+ sig << RBI::SigParam.new(param.param.name, param.type)
85
+ end
86
+ self << method
87
+ end
88
+
89
+ private
90
+
91
+ SPECIAL_METHOD_NAMES = T.let(
92
+ ["!", "~", "+@", "**", "-@", "*", "/", "%", "+", "-", "<<", ">>", "&", "|", "^", "<", "<=", "=>", ">", ">=",
93
+ "==", "===", "!=", "=~", "!~", "<=>", "[]", "[]=", "`"].freeze,
94
+ T::Array[String]
95
+ )
96
+
97
+ sig { params(name: String).returns(T::Boolean) }
98
+ def valid_method_name?(name)
99
+ return true if SPECIAL_METHOD_NAMES.include?(name)
100
+ !!name.match(/^[a-zA-Z_][[:word:]]*[?!=]?$/)
101
+ end
102
+
103
+ sig { returns(T::Hash[String, RBI::Node]) }
104
+ def nodes_cache
105
+ T.must(@nodes_cache ||= T.let({}, T.nilable(T::Hash[String, Node])))
106
+ end
107
+
108
+ sig { params(node: RBI::Node).returns(RBI::Node) }
109
+ def create_node(node)
110
+ cached = nodes_cache[node.to_s]
111
+ return cached if cached
112
+ nodes_cache[node.to_s] = node
113
+ self << node
114
+ node
115
+ end
116
+ end
117
+
118
+ class TypedParam < T::Struct
119
+ const :param, RBI::Param
120
+ const :type, String
121
+ end
122
+ end
@@ -0,0 +1,131 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Tapioca
5
+ module Reflection
6
+ extend T::Sig
7
+ extend self
8
+
9
+ CLASS_METHOD = T.let(Kernel.instance_method(:class), UnboundMethod)
10
+ CONSTANTS_METHOD = T.let(Module.instance_method(:constants), UnboundMethod)
11
+ NAME_METHOD = T.let(Module.instance_method(:name), UnboundMethod)
12
+ SINGLETON_CLASS_METHOD = T.let(Object.instance_method(:singleton_class), UnboundMethod)
13
+ ANCESTORS_METHOD = T.let(Module.instance_method(:ancestors), UnboundMethod)
14
+ SUPERCLASS_METHOD = T.let(Class.instance_method(:superclass), UnboundMethod)
15
+ OBJECT_ID_METHOD = T.let(BasicObject.instance_method(:__id__), UnboundMethod)
16
+ EQUAL_METHOD = T.let(BasicObject.instance_method(:equal?), UnboundMethod)
17
+ PUBLIC_INSTANCE_METHODS_METHOD = T.let(Module.instance_method(:public_instance_methods), UnboundMethod)
18
+ PROTECTED_INSTANCE_METHODS_METHOD = T.let(Module.instance_method(:protected_instance_methods), UnboundMethod)
19
+ PRIVATE_INSTANCE_METHODS_METHOD = T.let(Module.instance_method(:private_instance_methods), UnboundMethod)
20
+
21
+ sig { params(object: BasicObject).returns(Class).checked(:never) }
22
+ def class_of(object)
23
+ CLASS_METHOD.bind(object).call
24
+ end
25
+
26
+ sig { params(constant: Module).returns(T::Array[Symbol]) }
27
+ def constants_of(constant)
28
+ CONSTANTS_METHOD.bind(constant).call(false)
29
+ end
30
+
31
+ sig { params(constant: Module).returns(T.nilable(String)) }
32
+ def name_of(constant)
33
+ NAME_METHOD.bind(constant).call
34
+ end
35
+
36
+ sig { params(constant: Module).returns(Class) }
37
+ def singleton_class_of(constant)
38
+ SINGLETON_CLASS_METHOD.bind(constant).call
39
+ end
40
+
41
+ sig { params(constant: Module).returns(T::Array[Module]) }
42
+ def ancestors_of(constant)
43
+ ANCESTORS_METHOD.bind(constant).call
44
+ end
45
+
46
+ sig { params(constant: Class).returns(T.nilable(Class)) }
47
+ def superclass_of(constant)
48
+ SUPERCLASS_METHOD.bind(constant).call
49
+ end
50
+
51
+ sig { params(object: BasicObject).returns(Integer).checked(:never) }
52
+ def object_id_of(object)
53
+ OBJECT_ID_METHOD.bind(object).call
54
+ end
55
+
56
+ sig { params(object: BasicObject, other: BasicObject).returns(T::Boolean).checked(:never) }
57
+ def are_equal?(object, other)
58
+ EQUAL_METHOD.bind(object).call(other)
59
+ end
60
+
61
+ sig { params(constant: Module).returns(T::Array[Symbol]) }
62
+ def public_instance_methods_of(constant)
63
+ PUBLIC_INSTANCE_METHODS_METHOD.bind(constant).call
64
+ end
65
+
66
+ sig { params(constant: Module).returns(T::Array[Symbol]) }
67
+ def protected_instance_methods_of(constant)
68
+ PROTECTED_INSTANCE_METHODS_METHOD.bind(constant).call
69
+ end
70
+
71
+ sig { params(constant: Module).returns(T::Array[Symbol]) }
72
+ def private_instance_methods_of(constant)
73
+ PRIVATE_INSTANCE_METHODS_METHOD.bind(constant).call
74
+ end
75
+
76
+ sig { params(constant: Module).returns(T::Array[Module]) }
77
+ def inherited_ancestors_of(constant)
78
+ if Class === constant
79
+ ancestors_of(superclass_of(constant) || Object)
80
+ else
81
+ Module.ancestors
82
+ end
83
+ end
84
+
85
+ sig { params(constant: Module).returns(T.nilable(String)) }
86
+ def qualified_name_of(constant)
87
+ name = name_of(constant)
88
+ return if name.nil?
89
+
90
+ if name.start_with?("::")
91
+ name
92
+ else
93
+ "::#{name}"
94
+ end
95
+ end
96
+
97
+ sig { params(method: T.any(UnboundMethod, Method)).returns(T.untyped) }
98
+ def signature_of(method)
99
+ T::Private::Methods.signature_for_method(method)
100
+ rescue LoadError, StandardError
101
+ nil
102
+ end
103
+
104
+ sig { params(type: T::Types::Base).returns(String) }
105
+ def name_of_type(type)
106
+ type.to_s.gsub(/\bAttachedClass\b/, "T.attached_class")
107
+ end
108
+
109
+ # Returns an array with all classes that are < than the supplied class.
110
+ #
111
+ # class C; end
112
+ # descendants_of(C) # => []
113
+ #
114
+ # class B < C; end
115
+ # descendants_of(C) # => [B]
116
+ #
117
+ # class A < B; end
118
+ # descendants_of(C) # => [B, A]
119
+ #
120
+ # class D < C; end
121
+ # descendants_of(C) # => [B, A, D]
122
+ sig { type_parameters(:U).params(klass: T.type_parameter(:U)).returns(T::Array[T.type_parameter(:U)]) }
123
+ def descendants_of(klass)
124
+ result = ObjectSpace.each_object(klass.singleton_class).reject do |k|
125
+ T.cast(k, Module).singleton_class? || T.unsafe(k) == klass
126
+ end
127
+
128
+ T.unsafe(result)
129
+ end
130
+ end
131
+ end