tapioca 0.8.3 → 0.9.2

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