tapioca 0.8.3 → 0.9.2

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +5 -2
  3. data/README.md +188 -36
  4. data/lib/tapioca/cli.rb +130 -66
  5. data/lib/tapioca/commands/annotations.rb +167 -34
  6. data/lib/tapioca/commands/check_shims.rb +101 -0
  7. data/lib/tapioca/commands/{init.rb → configure.rb} +1 -1
  8. data/lib/tapioca/commands/dsl.rb +1 -1
  9. data/lib/tapioca/commands/gem.rb +15 -10
  10. data/lib/tapioca/commands.rb +2 -1
  11. data/lib/tapioca/dsl/compiler.rb +1 -13
  12. data/lib/tapioca/dsl/compilers/active_model_attributes.rb +1 -1
  13. data/lib/tapioca/dsl/compilers/active_record_relations.rb +17 -0
  14. data/lib/tapioca/dsl/compilers/active_record_typed_store.rb +5 -4
  15. data/lib/tapioca/dsl/compilers/frozen_record.rb +2 -2
  16. data/lib/tapioca/dsl/compilers/protobuf.rb +6 -0
  17. data/lib/tapioca/dsl/compilers.rb +0 -4
  18. data/lib/tapioca/dsl/helpers/active_record_column_type_helper.rb +21 -3
  19. data/lib/tapioca/dsl/pipeline.rb +0 -2
  20. data/lib/tapioca/dsl.rb +8 -0
  21. data/lib/tapioca/executor.rb +0 -3
  22. data/lib/tapioca/gem/events.rb +22 -3
  23. data/lib/tapioca/gem/listeners/base.rb +11 -0
  24. data/lib/tapioca/gem/listeners/dynamic_mixins.rb +5 -0
  25. data/lib/tapioca/gem/listeners/foreign_constants.rb +65 -0
  26. data/lib/tapioca/gem/listeners/methods.rb +7 -18
  27. data/lib/tapioca/gem/listeners/mixins.rb +31 -10
  28. data/lib/tapioca/gem/listeners/remove_empty_payload_scopes.rb +5 -0
  29. data/lib/tapioca/gem/listeners/sorbet_enums.rb +5 -0
  30. data/lib/tapioca/gem/listeners/sorbet_helpers.rb +5 -0
  31. data/lib/tapioca/gem/listeners/sorbet_props.rb +5 -0
  32. data/lib/tapioca/gem/listeners/sorbet_required_ancestors.rb +5 -0
  33. data/lib/tapioca/gem/listeners/sorbet_signatures.rb +6 -1
  34. data/lib/tapioca/gem/listeners/sorbet_type_variables.rb +5 -0
  35. data/lib/tapioca/gem/listeners/source_location.rb +67 -0
  36. data/lib/tapioca/gem/listeners/subconstants.rb +5 -0
  37. data/lib/tapioca/gem/listeners/yard_doc.rb +5 -0
  38. data/lib/tapioca/gem/listeners.rb +2 -0
  39. data/lib/tapioca/gem/pipeline.rb +64 -19
  40. data/lib/tapioca/gem.rb +6 -0
  41. data/lib/tapioca/gemfile.rb +7 -6
  42. data/lib/tapioca/helpers/cli_helper.rb +8 -2
  43. data/lib/tapioca/helpers/config_helper.rb +0 -2
  44. data/lib/tapioca/helpers/env_helper.rb +16 -0
  45. data/lib/tapioca/helpers/rbi_files_helper.rb +255 -0
  46. data/lib/tapioca/helpers/rbi_helper.rb +98 -94
  47. data/lib/tapioca/helpers/sorbet_helper.rb +2 -3
  48. data/lib/tapioca/helpers/test/content.rb +0 -2
  49. data/lib/tapioca/helpers/test/template.rb +0 -2
  50. data/lib/tapioca/internal.rb +36 -12
  51. data/lib/tapioca/rbi_ext/model.rb +2 -15
  52. data/lib/tapioca/runtime/dynamic_mixin_compiler.rb +18 -16
  53. data/lib/tapioca/runtime/reflection.rb +26 -0
  54. data/lib/tapioca/runtime/trackers/constant_definition.rb +44 -16
  55. data/lib/tapioca/runtime/trackers/mixin.rb +49 -14
  56. data/lib/tapioca/sorbet_ext/generic_name_patch.rb +1 -4
  57. data/lib/tapioca/sorbet_ext/name_patch.rb +15 -5
  58. data/lib/tapioca/sorbet_ext/proc_bind_patch.rb +40 -0
  59. data/lib/tapioca/static/requires_compiler.rb +0 -2
  60. data/lib/tapioca/static/symbol_loader.rb +26 -30
  61. data/lib/tapioca/static/symbol_table_parser.rb +0 -3
  62. data/lib/tapioca/version.rb +1 -1
  63. data/lib/tapioca.rb +3 -0
  64. metadata +24 -7
  65. data/lib/tapioca/dsl/helpers/param_helper.rb +0 -55
  66. data/lib/tapioca/helpers/shims_helper.rb +0 -115
  67. data/lib/tapioca/helpers/signatures_helper.rb +0 -17
  68. data/lib/tapioca/helpers/type_variable_helper.rb +0 -43
@@ -4,121 +4,125 @@
4
4
  module Tapioca
5
5
  module RBIHelper
6
6
  extend T::Sig
7
- extend T::Helpers
7
+ include SorbetHelper
8
+ extend SorbetHelper
9
+ extend self
8
10
 
9
- requires_ancestor { Thor::Shell }
10
- requires_ancestor { SorbetHelper }
11
-
12
- sig do
13
- params(
14
- command: String,
15
- gem_dir: String,
16
- dsl_dir: String,
17
- auto_strictness: T::Boolean,
18
- gems: T::Array[Gemfile::GemSpec],
19
- compilers: T::Enumerable[Class]
20
- ).void
11
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
12
+ def create_param(name, type:)
13
+ create_typed_param(RBI::Param.new(name), type)
21
14
  end
22
- def validate_rbi_files(command:, gem_dir:, dsl_dir:, auto_strictness:, gems: [], compilers: [])
23
- error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
24
-
25
- say("Checking generated RBI files... ")
26
- res = sorbet(
27
- "--no-config",
28
- "--error-url-base=#{error_url_base}",
29
- "--stop-after namer",
30
- dsl_dir,
31
- gem_dir
32
- )
33
- say(" Done", :green)
34
-
35
- errors = Spoom::Sorbet::Errors::Parser.parse_string(res.err)
36
-
37
- if errors.empty?
38
- say(" No errors found\n\n", [:green, :bold])
39
- return
40
- end
41
-
42
- parse_errors = errors.select { |error| error.code < 4000 }
43
-
44
- if parse_errors.any?
45
- say_error(<<~ERR, :red)
46
-
47
- ##### INTERNAL ERROR #####
48
15
 
49
- There are parse errors in the generated RBI files.
50
-
51
- This seems related to a bug in Tapioca.
52
- Please open an issue at https://github.com/Shopify/tapioca/issues/new with the following information:
53
-
54
- Tapioca v#{Tapioca::VERSION}
55
-
56
- Command:
57
- #{command}
58
-
59
- ERR
60
-
61
- say_error(<<~ERR, :red) if gems.any?
62
- Gems:
63
- #{gems.map { |gem| " #{gem.name} (#{gem.version})" }.join("\n")}
16
+ sig { params(name: String, type: String, default: String).returns(RBI::TypedParam) }
17
+ def create_opt_param(name, type:, default:)
18
+ create_typed_param(RBI::OptParam.new(name, default), type)
19
+ end
64
20
 
65
- ERR
21
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
22
+ def create_rest_param(name, type:)
23
+ create_typed_param(RBI::RestParam.new(name), type)
24
+ end
66
25
 
67
- say_error(<<~ERR, :red) if compilers.any?
68
- Compilers:
69
- #{compilers.map { |compiler| " #{compiler.name}" }.join("\n")}
26
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
27
+ def create_kw_param(name, type:)
28
+ create_typed_param(RBI::KwParam.new(name), type)
29
+ end
70
30
 
71
- ERR
31
+ sig { params(name: String, type: String, default: String).returns(RBI::TypedParam) }
32
+ def create_kw_opt_param(name, type:, default:)
33
+ create_typed_param(RBI::KwOptParam.new(name, default), type)
34
+ end
72
35
 
73
- say_error(<<~ERR, :red)
74
- Errors:
75
- #{parse_errors.map { |error| " #{error}" }.join("\n")}
36
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
37
+ def create_kw_rest_param(name, type:)
38
+ create_typed_param(RBI::KwRestParam.new(name), type)
39
+ end
76
40
 
77
- ##########################
41
+ sig { params(name: String, type: String).returns(RBI::TypedParam) }
42
+ def create_block_param(name, type:)
43
+ create_typed_param(RBI::BlockParam.new(name), type)
44
+ end
78
45
 
79
- ERR
80
- end
46
+ sig { params(param: RBI::Param, type: String).returns(RBI::TypedParam) }
47
+ def create_typed_param(param, type)
48
+ RBI::TypedParam.new(param: param, type: sanitize_signature_types(type))
49
+ end
81
50
 
82
- if auto_strictness
83
- redef_errors = errors.select { |error| error.code == 4010 }
84
- update_gem_rbis_strictnesses(redef_errors, gem_dir)
85
- end
51
+ sig { params(sig_string: String).returns(String) }
52
+ def sanitize_signature_types(sig_string)
53
+ sig_string
54
+ .gsub(".returns(<VOID>)", ".void")
55
+ .gsub("<VOID>", "void")
56
+ .gsub("<NOT-TYPED>", "T.untyped")
57
+ .gsub(".params()", "")
58
+ end
86
59
 
87
- Kernel.exit(1) if parse_errors.any?
60
+ sig do
61
+ params(
62
+ type: String,
63
+ variance: Symbol,
64
+ fixed: T.nilable(String),
65
+ upper: T.nilable(String),
66
+ lower: T.nilable(String)
67
+ ).returns(String)
88
68
  end
69
+ def self.serialize_type_variable(type, variance, fixed, upper, lower)
70
+ variance = nil if variance == :invariant
89
71
 
90
- private
72
+ bounds = []
73
+ bounds << "fixed: #{fixed}" if fixed
74
+ bounds << "lower: #{lower}" if lower
75
+ bounds << "upper: #{upper}" if upper
91
76
 
92
- sig { params(errors: T::Array[Spoom::Sorbet::Errors::Error], gem_dir: String).void }
93
- def update_gem_rbis_strictnesses(errors, gem_dir)
94
- files = []
77
+ parameters = []
78
+ block = []
95
79
 
96
- errors.each do |error|
97
- # Collect the file with error
98
- files << error.file
99
- error.more.each do |line|
100
- # Also collect the conflicting definition file paths
101
- next unless line.include?("Previous definition")
80
+ parameters << ":#{variance}" if variance
102
81
 
103
- files << line.split(":").first&.strip
104
- end
82
+ if sorbet_supports?(:type_variable_block_syntax)
83
+ block = bounds
84
+ else
85
+ parameters.concat(bounds)
105
86
  end
106
87
 
107
- files
108
- .uniq
109
- .sort
110
- .select { |file| file.start_with?(gem_dir) }
111
- .each do |file|
112
- Spoom::Sorbet::Sigils.change_sigil_in_file(file, "false")
113
- say("\n Changed strictness of #{file} to `typed: false` (conflicting with DSL files)", [:yellow, :bold])
114
- end
88
+ serialized = type.dup
89
+ serialized << "(#{parameters.join(", ")})" unless parameters.empty?
90
+ serialized << " { { #{block.join(", ")} } }" unless block.empty?
91
+ serialized
92
+ end
115
93
 
116
- say("\n")
94
+ sig { params(name: String).returns(T::Boolean) }
95
+ def valid_method_name?(name)
96
+ # try to parse a method definition with this name
97
+ iseq = RubyVM::InstructionSequence.compile("def #{name}; end", nil, nil, 0, false)
98
+ # pull out the first operation in the instruction sequence and its first argument
99
+ op, arg, _data = iseq.to_a.dig(-1, 0)
100
+ # make sure that the operation is a method definition and the method that was
101
+ # defined has the expected name, for example, for `def !foo; end` we don't get
102
+ # a syntax error but instead get a method defined as `"foo"`
103
+ op == :definemethod && arg == name.to_sym
104
+ rescue SyntaxError
105
+ false
117
106
  end
118
107
 
119
- sig { params(path: String).returns(String) }
120
- def gem_name_from_rbi_path(path)
121
- T.must(File.basename(path, ".rbi").split("@").first)
108
+ sig { params(name: String).returns(T::Boolean) }
109
+ def valid_parameter_name?(name)
110
+ sentinel_method_name = :sentinel_method_name
111
+ # try to parse a method definition with this name as the name of a
112
+ # keyword parameter. If we use a positional parameter, then parameter names
113
+ # like `&` (and maybe others) will be treated like `def foo(&); end` and will
114
+ # thus be considered valid. Using a required keyword parameter prevents that
115
+ # confusion between Ruby syntax and parameter name.
116
+ iseq = RubyVM::InstructionSequence.compile("def #{sentinel_method_name}(#{name}:); end", nil, nil, 0, false)
117
+ # pull out the first operation in the instruction sequence and its first argument and data
118
+ op, arg, data = iseq.to_a.dig(-1, 0)
119
+ # make sure that:
120
+ # 1. a method was defined, and
121
+ # 2. the method has the expected method name, and
122
+ # 3. the method has a keyword parameter with the expected name
123
+ op == :definemethod && arg == sentinel_method_name && data.dig(11, :keyword, 0) == name.to_sym
124
+ rescue SyntaxError
125
+ false
122
126
  end
123
127
  end
124
128
  end
@@ -1,9 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "pathname"
5
- require "shellwords"
6
-
7
4
  module Tapioca
8
5
  module SorbetHelper
9
6
  extend T::Sig
@@ -20,6 +17,8 @@ module Tapioca
20
17
 
21
18
  SORBET_EXE_PATH_ENV_VAR = "TAPIOCA_SORBET_EXE"
22
19
 
20
+ SORBET_PAYLOAD_URL = "https://github.com/sorbet/sorbet/tree/master/rbi"
21
+
23
22
  FEATURE_REQUIREMENTS = T.let({
24
23
  to_ary_nil_support: ::Gem::Requirement.new(">= 0.5.9220"), # https://github.com/sorbet/sorbet/pull/4706
25
24
  print_payload_sources: ::Gem::Requirement.new(">= 0.5.9818"), # https://github.com/sorbet/sorbet/pull/5504
@@ -1,8 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "fileutils"
5
-
6
4
  module Tapioca
7
5
  module Helpers
8
6
  module Test
@@ -1,8 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
- require "erb"
5
-
6
4
  module Tapioca
7
5
  module Helpers
8
6
  module Test
@@ -1,32 +1,56 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "set"
5
+
4
6
  require "tapioca"
5
7
  require "tapioca/runtime/reflection"
6
8
  require "tapioca/runtime/trackers"
9
+
10
+ require "benchmark"
11
+ require "bundler"
12
+ require "erb"
13
+ require "etc"
14
+ require "fileutils"
15
+ require "json"
16
+ require "logger"
17
+ require "net/http"
18
+ require "netrc"
19
+ require "parallel"
20
+ require "pathname"
21
+ require "shellwords"
22
+ require "spoom"
23
+ require "tempfile"
24
+ require "thor"
25
+ require "yaml"
26
+ require "yard-sorbet"
27
+
7
28
  require "tapioca/runtime/dynamic_mixin_compiler"
8
29
  require "tapioca/helpers/gem_helper"
9
30
  require "tapioca/runtime/loader"
31
+
10
32
  require "tapioca/helpers/sorbet_helper"
11
- require "tapioca/helpers/type_variable_helper"
12
- require "tapioca/sorbet_ext/generic_name_patch"
33
+ require "tapioca/helpers/rbi_helper"
13
34
  require "tapioca/sorbet_ext/fixed_hash_patch"
35
+ require "tapioca/sorbet_ext/name_patch"
36
+ require "tapioca/sorbet_ext/generic_name_patch"
37
+ require "tapioca/sorbet_ext/proc_bind_patch"
14
38
  require "tapioca/runtime/generic_type_registry"
39
+
15
40
  require "tapioca/helpers/cli_helper"
16
41
  require "tapioca/helpers/config_helper"
17
- require "tapioca/helpers/signatures_helper"
18
- require "tapioca/helpers/rbi_helper"
19
- require "tapioca/helpers/shims_helper"
42
+ require "tapioca/helpers/rbi_files_helper"
43
+ require "tapioca/helpers/env_helper"
44
+
20
45
  require "tapioca/repo_index"
21
- require "tapioca/commands"
22
- require "tapioca/cli"
23
46
  require "tapioca/gemfile"
24
47
  require "tapioca/executor"
48
+
25
49
  require "tapioca/static/symbol_table_parser"
26
50
  require "tapioca/static/symbol_loader"
27
- require "tapioca/gem/events"
28
- require "tapioca/gem/listeners"
29
- require "tapioca/gem/pipeline"
30
- require "tapioca/dsl/compiler"
31
- require "tapioca/dsl/pipeline"
32
51
  require "tapioca/static/requires_compiler"
52
+
53
+ require "tapioca/gem"
54
+ require "tapioca/dsl"
55
+ require "tapioca/commands"
56
+ require "tapioca/cli"
@@ -72,7 +72,7 @@ module RBI
72
72
  ).void
73
73
  end
74
74
  def create_type_variable(name, type:, variance: :invariant, fixed: nil, upper: nil, lower: nil)
75
- value = Tapioca::TypeVariableHelper.serialize_type_variable(type, variance, fixed, upper, lower)
75
+ value = Tapioca::RBIHelper.serialize_type_variable(type, variance, fixed, upper, lower)
76
76
  create_node(RBI::TypeMember.new(name, value))
77
77
  end
78
78
 
@@ -86,7 +86,7 @@ module RBI
86
86
  ).void
87
87
  end
88
88
  def create_method(name, parameters: [], return_type: "T.untyped", class_method: false, visibility: RBI::Public.new)
89
- return unless valid_method_name?(name)
89
+ return unless Tapioca::RBIHelper.valid_method_name?(name)
90
90
 
91
91
  sig = RBI::Sig.new(return_type: return_type)
92
92
  method = RBI::Method.new(name, sigs: [sig], is_singleton: class_method, visibility: visibility)
@@ -99,19 +99,6 @@ module RBI
99
99
 
100
100
  private
101
101
 
102
- SPECIAL_METHOD_NAMES = T.let(
103
- ["!", "~", "+@", "**", "-@", "*", "/", "%", "+", "-", "<<", ">>", "&", "|", "^", "<", "<=", "=>", ">", ">=",
104
- "==", "===", "!=", "=~", "!~", "<=>", "[]", "[]=", "`",].freeze,
105
- T::Array[String]
106
- )
107
-
108
- sig { params(name: String).returns(T::Boolean) }
109
- def valid_method_name?(name)
110
- return true if SPECIAL_METHOD_NAMES.include?(name)
111
-
112
- !!name.match(/^[a-zA-Z_][[:word:]]*[?!=]?$/)
113
- end
114
-
115
102
  sig { returns(T::Hash[String, RBI::Node]) }
116
103
  def nodes_cache
117
104
  T.must(@nodes_cache ||= T.let({}, T.nilable(T::Hash[String, Node])))
@@ -35,22 +35,24 @@ module Tapioca
35
35
  # before the actual include
36
36
  before = singleton_class.ancestors
37
37
  # Call the actual `include` method with the supplied module
38
- super(mod).tap do
39
- # Take a snapshot of the list of singleton class ancestors
40
- # after the actual include
41
- after = singleton_class.ancestors
42
- # The difference is the modules that are added to the list
43
- # of ancestors of the singleton class. Those are all the
44
- # modules that were `extend`ed due to the `include` call.
45
- #
46
- # We record those modules on our lookup table keyed by
47
- # the included module with the values being all the modules
48
- # that that module pulls into the singleton class.
49
- #
50
- # We need to reverse the order, since the extend order should
51
- # be the inverse of the ancestor order. That is, earlier
52
- # extended modules would be later in the ancestor chain.
53
- mixins_from_modules[mod] = (after - before).reverse!
38
+ ::Tapioca::Runtime::Trackers::Mixin.with_disabled_registration do
39
+ super(mod).tap do
40
+ # Take a snapshot of the list of singleton class ancestors
41
+ # after the actual include
42
+ after = singleton_class.ancestors
43
+ # The difference is the modules that are added to the list
44
+ # of ancestors of the singleton class. Those are all the
45
+ # modules that were `extend`ed due to the `include` call.
46
+ #
47
+ # We record those modules on our lookup table keyed by
48
+ # the included module with the values being all the modules
49
+ # that that module pulls into the singleton class.
50
+ #
51
+ # We need to reverse the order, since the extend order should
52
+ # be the inverse of the ancestor order. That is, earlier
53
+ # extended modules would be later in the ancestor chain.
54
+ mixins_from_modules[mod] = (after - before).reverse!
55
+ end
54
56
  end
55
57
  rescue Exception # rubocop:disable Lint/RescueException
56
58
  # this is a best effort, bail if we can't perform this
@@ -20,6 +20,8 @@ module Tapioca
20
20
  PRIVATE_INSTANCE_METHODS_METHOD = T.let(Module.instance_method(:private_instance_methods), UnboundMethod)
21
21
  METHOD_METHOD = T.let(Kernel.instance_method(:method), UnboundMethod)
22
22
 
23
+ REQUIRED_FROM_LABELS = T.let(["<top (required)>", "<main>"].freeze, T::Array[String])
24
+
23
25
  sig do
24
26
  params(
25
27
  symbol: String,
@@ -152,6 +154,30 @@ module Tapioca
152
154
 
153
155
  T.unsafe(result)
154
156
  end
157
+
158
+ # Examines the call stack to identify the closest location where a "require" is performed
159
+ # by searching for the label "<top (required)>". If none is found, it returns the location
160
+ # labeled "<main>", which is the original call site.
161
+ sig { params(locations: T.nilable(T::Array[Thread::Backtrace::Location])).returns(String) }
162
+ def resolve_loc(locations)
163
+ return "" unless locations
164
+
165
+ resolved_loc = locations.find { |loc| REQUIRED_FROM_LABELS.include?(loc.label) }
166
+ return "" unless resolved_loc
167
+
168
+ resolved_loc.absolute_path || ""
169
+ end
170
+
171
+ sig { params(constant: Module).returns(T.nilable(String)) }
172
+ def constant_name_from_singleton_class(constant)
173
+ constant.to_s.match("#<Class:(.+)>")&.captures&.first
174
+ end
175
+
176
+ sig { params(constant: Module).returns(T.nilable(BasicObject)) }
177
+ def constant_from_singleton_class(constant)
178
+ constant_name = constant_name_from_singleton_class(constant)
179
+ constantize(constant_name) if constant_name
180
+ end
155
181
  end
156
182
  end
157
183
  end
@@ -1,8 +1,6 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
- require "set"
5
-
6
4
  module Tapioca
7
5
  module Runtime
8
6
  module Trackers
@@ -12,31 +10,61 @@ module Tapioca
12
10
  # available in the ruby runtime without extra accounting.
13
11
  module ConstantDefinition
14
12
  extend Reflection
13
+ extend T::Sig
14
+
15
+ class ConstantLocation < T::Struct
16
+ const :lineno, Integer
17
+ const :path, String
18
+ end
15
19
 
16
- @class_files = {}
20
+ @class_files = {}.compare_by_identity
17
21
 
18
22
  # Immediately activated upon load. Observes class/module definition.
19
23
  TracePoint.trace(:class) do |tp|
20
- unless tp.self.singleton_class?
21
- key = name_of(tp.self)
22
- file = tp.path
23
- if file == "(eval)"
24
- file = T.must(caller_locations)
25
- .drop_while { |loc| loc.path == "(eval)" }
26
- .first&.path
27
- end
28
- @class_files[key] ||= Set.new
29
- @class_files[key] << file
24
+ next if tp.self.singleton_class?
25
+
26
+ key = tp.self
27
+
28
+ path = tp.path
29
+ if File.exist?(path)
30
+ loc = build_constant_location(tp, caller_locations)
31
+ else
32
+ caller_location = T.must(caller_locations)
33
+ .find { |loc| loc.path && File.exist?(loc.path) }
34
+
35
+ next unless caller_location
36
+
37
+ loc = ConstantLocation.new(path: caller_location.absolute_path || "", lineno: caller_location.lineno)
30
38
  end
39
+
40
+ (@class_files[key] ||= Set.new) << loc
41
+ end
42
+
43
+ TracePoint.trace(:c_return) do |tp|
44
+ next unless tp.method_id == :new
45
+ next unless Module === tp.return_value
46
+
47
+ key = tp.return_value
48
+ loc = build_constant_location(tp, caller_locations)
49
+ (@class_files[key] ||= Set.new) << loc
50
+ end
51
+
52
+ def self.build_constant_location(tp, locations)
53
+ file = resolve_loc(caller_locations)
54
+ lineno = file == File.realpath(tp.path) ? tp.lineno : 0
55
+
56
+ ConstantLocation.new(path: file, lineno: lineno)
31
57
  end
32
58
 
33
59
  # Returns the files in which this class or module was opened. Doesn't know
34
60
  # about situations where the class was opened prior to +require+ing,
35
61
  # or where metaprogramming was used via +eval+, etc.
36
62
  def self.files_for(klass)
37
- name = String === klass ? klass : name_of(klass)
38
- files = @class_files[name]
39
- files || Set.new
63
+ locations_for(klass).map(&:path).to_set
64
+ end
65
+
66
+ def self.locations_for(klass)
67
+ @class_files.fetch(klass, Set.new)
40
68
  end
41
69
  end
42
70
  end
@@ -7,7 +7,9 @@ module Tapioca
7
7
  module Mixin
8
8
  extend T::Sig
9
9
 
10
- @mixin_map = {}.compare_by_identity
10
+ @constants_to_mixin_locations = {}.compare_by_identity
11
+ @mixins_to_constants = {}.compare_by_identity
12
+ @enabled = true
11
13
 
12
14
  class Type < T::Enum
13
15
  enums do
@@ -17,24 +19,38 @@ module Tapioca
17
19
  end
18
20
  end
19
21
 
22
+ sig do
23
+ type_parameters(:Result)
24
+ .params(block: T.proc.returns(T.type_parameter(:Result)))
25
+ .returns(T.type_parameter(:Result))
26
+ end
27
+ def self.with_disabled_registration(&block)
28
+ @enabled = false
29
+
30
+ block.call
31
+ ensure
32
+ @enabled = true
33
+ end
34
+
20
35
  sig do
21
36
  params(
22
37
  constant: Module,
23
- mod: Module,
38
+ mixin: Module,
24
39
  mixin_type: Type,
25
- locations: T.nilable(T::Array[Thread::Backtrace::Location])
26
40
  ).void
27
41
  end
28
- def self.register(constant, mod, mixin_type, locations)
29
- locations ||= []
30
- locations.map!(&:absolute_path).uniq!
31
- locs = mixin_locations_for(constant)
32
- locs.fetch(mixin_type).store(mod, T.cast(locations, T::Array[String]))
42
+ def self.register(constant, mixin, mixin_type)
43
+ return unless @enabled
44
+
45
+ location = Reflection.resolve_loc(caller_locations)
46
+
47
+ constants = constants_with_mixin(mixin)
48
+ constants.fetch(mixin_type).store(constant, location)
33
49
  end
34
50
 
35
- sig { params(constant: Module).returns(T::Hash[Type, T::Hash[Module, T::Array[String]]]) }
36
- def self.mixin_locations_for(constant)
37
- @mixin_map[constant] ||= {
51
+ sig { params(mixin: Module).returns(T::Hash[Type, T::Hash[Module, String]]) }
52
+ def self.constants_with_mixin(mixin)
53
+ @mixins_to_constants[mixin] ||= {
38
54
  Type::Prepend => {}.compare_by_identity,
39
55
  Type::Include => {}.compare_by_identity,
40
56
  Type::Extend => {}.compare_by_identity,
@@ -52,8 +68,10 @@ class Module
52
68
  constant,
53
69
  self,
54
70
  Tapioca::Runtime::Trackers::Mixin::Type::Prepend,
55
- caller_locations
56
71
  )
72
+
73
+ register_extend_on_attached_class(constant) if constant.singleton_class?
74
+
57
75
  super
58
76
  end
59
77
 
@@ -62,8 +80,10 @@ class Module
62
80
  constant,
63
81
  self,
64
82
  Tapioca::Runtime::Trackers::Mixin::Type::Include,
65
- caller_locations
66
83
  )
84
+
85
+ register_extend_on_attached_class(constant) if constant.singleton_class?
86
+
67
87
  super
68
88
  end
69
89
 
@@ -72,9 +92,24 @@ class Module
72
92
  obj,
73
93
  self,
74
94
  Tapioca::Runtime::Trackers::Mixin::Type::Extend,
75
- caller_locations
76
95
  ) if Module === obj
77
96
  super
78
97
  end
98
+
99
+ private
100
+
101
+ # Including or prepending on a singleton class is functionally equivalent to extending the
102
+ # attached class. Registering the mixin as an extend on the attached class ensures that
103
+ # this mixin can be found whether searching for an include/prepend on the singleton class
104
+ # or an extend on the attached class.
105
+ def register_extend_on_attached_class(constant)
106
+ attached_class = Tapioca::Runtime::Reflection.constant_from_singleton_class(constant)
107
+
108
+ Tapioca::Runtime::Trackers::Mixin.register(
109
+ T.cast(attached_class, Module),
110
+ self,
111
+ Tapioca::Runtime::Trackers::Mixin::Type::Extend,
112
+ ) if attached_class
113
+ end
79
114
  end)
80
115
  end
@@ -1,9 +1,6 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
- require "tapioca/sorbet_ext/name_patch"
5
- require "tapioca/helpers/sorbet_helper"
6
-
7
4
  module T
8
5
  module Generic
9
6
  # This module intercepts calls to generic type instantiations and type variable definitions.
@@ -174,7 +171,7 @@ module Tapioca
174
171
  lower = bounds[:lower].to_s if bounds.key?(:lower)
175
172
  upper = bounds[:upper].to_s if bounds.key?(:upper)
176
173
 
177
- TypeVariableHelper.serialize_type_variable(
174
+ RBIHelper.serialize_type_variable(
178
175
  @type.serialize,
179
176
  @variance,
180
177
  fixed,