tapioca 0.6.0 → 0.6.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a99e7a4475e2991e305c461774e45e128d430632e96581fc541680b61275e4e1
4
- data.tar.gz: 8ee7b9b225dd752edf606a98aee1b8490687f351dae642b24060549740d2ab5a
3
+ metadata.gz: 6ea7bddec4d5d00918ca3236afee754f7f8dd9481d78bf8e26819c0140b154ff
4
+ data.tar.gz: 681d5ed15d27c20833aa4df86dc1a61d721e0d5993a43073873f2406a254d806
5
5
  SHA512:
6
- metadata.gz: 3ab85b1422fba3aa79de02fad9e164b2013cef3a388ad196e4c11f0c2c6dc142b6a540a430f4a4ace9f3ebe92da2f29fbe733e13ceb481277e7d7e2691db9f0c
7
- data.tar.gz: b9f2c2cca77916e08fcbb1ca021818b51bc5edd8d899b2f5e89dae3bfddd38dd8e403b16bacd8c8db37dc630448458d7d6f52e6445cb9ecb96c6bba16c4c20e7
6
+ metadata.gz: a2e885fa0010d90899b1f167e3a68c1d65f7cec21d7386a321a51d753fdf6e4deaf97d5c0866b7969cf90f42a5aa68af858e173237f8fc0a7c31bc08c3931185
7
+ data.tar.gz: a48f142ad8402b07c2b4724801b2dee912f1b95975bb9951feb97aef433a46c248e458527827be3a450336f2c62195714a5f5b476ed7704dad4ccefdb93aca98
data/Gemfile CHANGED
@@ -35,4 +35,10 @@ group(:development, :test) do
35
35
  gem("config", require: false)
36
36
  gem("aasm", require: false)
37
37
  gem("bcrypt", require: false)
38
+ gem("xpath", require: false)
39
+
40
+ # net-smtp was removed from default gems in Ruby 3.1, but is used by the `mail` gem.
41
+ # So we need to add it as a dependency until `mail` is fixed:
42
+ # https://github.com/rails/rails/blob/0919aa97260ab8240150278d3b07a1547489e3fd/Gemfile#L178-L191
43
+ gem("net-smtp", "0.3.1", require: false)
38
44
  end
data/README.md CHANGED
@@ -80,8 +80,8 @@ Command: `tapioca init`
80
80
 
81
81
  This will create the `sorbet/config` and `sorbet/tapioca/require.rb` files for you, if they don't exist. If any of the files already exist, they will not be changed.
82
82
 
83
+ <!-- START_HELP_COMMAND_INIT -->
83
84
  ```shell
84
- $ bundle exec tapioca help init
85
85
  Usage:
86
86
  tapioca init
87
87
 
@@ -92,6 +92,7 @@ Options:
92
92
 
93
93
  initializes folder structure
94
94
  ```
95
+ <!-- END_HELP_COMMAND_INIT -->
95
96
 
96
97
  ### Generate RBI files for gems
97
98
 
@@ -99,13 +100,13 @@ Command: `tapioca gem [gems...]`
99
100
 
100
101
  This will generate RBIs for the specified gems and place them in the RBI directory.
101
102
 
103
+ <!-- START_HELP_COMMAND_GEM -->
102
104
  ```shell
103
- $ bundle exec tapioca help gem
104
105
  Usage:
105
106
  tapioca gem [gem...]
106
107
 
107
108
  Options:
108
- --out, -o, [--outdir=directory] # The output directory for generated RBI files
109
+ --out, -o, [--outdir=directory] # The output directory for generated gem RBI files
109
110
  # Default: sorbet/rbi/gems
110
111
  [--file-header], [--no-file-header] # Add a "This file is generated" header on top of each generated RBI file
111
112
  # Default: true
@@ -113,10 +114,10 @@ Options:
113
114
  --pre, -b, [--prerequire=file] # A file to be required before Bundler.require is called
114
115
  --post, -a, [--postrequire=file] # A file to be required after Bundler.require is called
115
116
  # Default: sorbet/tapioca/require.rb
116
- -x, [--exclude=gem [gem ...]] # Excludes the given gem(s) from RBI generation
117
- --typed, -t, [--typed-overrides=gem:level [gem:level ...]] # Overrides for typed sigils for generated gem RBIs
117
+ -x, [--exclude=gem [gem ...]] # Exclude the given gem(s) from RBI generation
118
+ --typed, -t, [--typed-overrides=gem:level [gem:level ...]] # Override for typed sigils for generated gem RBIs
118
119
  # Default: {"activesupport"=>"false"}
119
- [--verify], [--no-verify] # Verifies RBIs are up-to-date
120
+ [--verify], [--no-verify] # Verify RBIs are up-to-date
120
121
  [--doc], [--no-doc] # Include YARD documentation from sources when generating RBIs. Warning: this might be slow
121
122
  [--exported-gem-rbis], [--no-exported-gem-rbis] # Include RBIs found in the `rbi/` directory of the gem
122
123
  # Default: true
@@ -128,6 +129,7 @@ Options:
128
129
 
129
130
  generate RBIs from gems
130
131
  ```
132
+ <!-- END_HELP_COMMAND_GEM -->
131
133
 
132
134
  ### Generate the list of all unresolved constants
133
135
 
@@ -135,13 +137,13 @@ Command: `tapioca todo`
135
137
 
136
138
  This will generate the file `sorbet/rbi/todo.rbi` defining all unresolved constants as empty modules.
137
139
 
140
+ <!-- START_HELP_COMMAND_TODO -->
138
141
  ```shell
139
- $ bundle exec tapioca help todo
140
142
  Usage:
141
143
  tapioca todo
142
144
 
143
145
  Options:
144
- [--todo-file=TODO_FILE]
146
+ [--todo-file=TODO_FILE] # Path to the generated todo RBI file
145
147
  # Default: sorbet/rbi/todo.rbi
146
148
  [--file-header], [--no-file-header] # Add a "This file is generated" header on top of each generated RBI file
147
149
  # Default: true
@@ -151,6 +153,7 @@ Options:
151
153
 
152
154
  generate the list of unresolved constants
153
155
  ```
156
+ <!-- END_HELP_COMMAND_TODO -->
154
157
 
155
158
  ### Generate DSL RBI files
156
159
 
@@ -158,18 +161,18 @@ Command: `tapioca dsl [constant...]`
158
161
 
159
162
  This will generate DSL RBIs for specified constants (or for all handled constants, if a constant name is not supplied). You can read about DSL RBI generators supplied by `tapioca` in [the manual](manual/generators.md).
160
163
 
164
+ <!-- START_HELP_COMMAND_DSL -->
161
165
  ```shell
162
- $ bundle exec tapioca help dsl
163
166
  Usage:
164
167
  tapioca dsl [constant...]
165
168
 
166
169
  Options:
167
- --out, -o, [--outdir=directory] # The output directory for generated RBI files
170
+ --out, -o, [--outdir=directory] # The output directory for generated DSL RBI files
168
171
  # Default: sorbet/rbi/dsl
169
172
  [--file-header], [--no-file-header] # Add a "This file is generated" header on top of each generated RBI file
170
173
  # Default: true
171
- [--only=generator [generator ...]] # Only run supplied DSL generators
172
- [--exclude=generator [generator ...]] # Exclude supplied DSL generators
174
+ [--only=generator [generator ...]] # Only run supplied DSL generator(s)
175
+ [--exclude=generator [generator ...]] # Exclude supplied DSL generator(s)
173
176
  [--verify], [--no-verify] # Verifies RBIs are up-to-date
174
177
  -q, [--quiet], [--no-quiet] # Supresses file creation output
175
178
  -w, [--workers=N] # EXPERIMENTAL: Number of parallel workers to use when generating RBIs
@@ -180,6 +183,8 @@ Options:
180
183
 
181
184
  generate RBIs for dynamic methods
182
185
  ```
186
+ <!-- END_HELP_COMMAND_DSL -->
187
+
183
188
  ## Configuration
184
189
 
185
190
  Tapioca supports loading command defaults from a configuration file. The default configuration
@@ -192,20 +197,57 @@ For example, if you always want to generate gem RBIs with inline documentation,
192
197
 
193
198
  ```yaml
194
199
  gem:
195
- docs: true
200
+ doc: true
196
201
  ```
197
202
 
198
203
  Additionally, if you always want to exclude the `AASM` and `ActiveRecordFixtures` DSL compilers in your DSL RBI generation runs, your config file would then look like this:
199
204
 
200
205
  ```yaml
201
206
  gem:
202
- docs: true
207
+ doc: true
203
208
  dsl:
204
209
  exclude:
205
210
  - UrlHelpers
206
211
  - ActiveRecordFixtures
207
212
  ```
208
213
 
214
+ The full configuration file, with each option and its default value, would look something like this:
215
+ <!-- START_CONFIG_TEMPLATE -->
216
+ ```yaml
217
+ ---
218
+ require:
219
+ postrequire: sorbet/tapioca/require.rb
220
+ todo:
221
+ todo_file: sorbet/rbi/todo.rbi
222
+ file_header: true
223
+ dsl:
224
+ outdir: sorbet/rbi/dsl
225
+ file_header: true
226
+ only: []
227
+ exclude: []
228
+ verify: false
229
+ quiet: false
230
+ workers: 1
231
+ gem:
232
+ outdir: sorbet/rbi/gems
233
+ file_header: true
234
+ all: false
235
+ prerequire: ''
236
+ postrequire: sorbet/tapioca/require.rb
237
+ exclude: []
238
+ typed_overrides:
239
+ activesupport: 'false'
240
+ verify: false
241
+ doc: false
242
+ exported_gem_rbis: true
243
+ workers: 1
244
+ clean_shims:
245
+ gem_rbi_dir: sorbet/rbi/gems
246
+ dsl_rbi_dir: sorbet/rbi/dsl
247
+ shim_rbi_dir: sorbet/rbi/shims
248
+ ```
249
+ <!-- END_CONFIG_TEMPLATE -->
250
+
209
251
  ## Contributing
210
252
 
211
253
  Bug reports and pull requests are welcome on GitHub at https://github.com/Shopify/tapioca. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](https://github.com/Shopify/tapioca/blob/main/CODE_OF_CONDUCT.md) code of conduct.
data/lib/tapioca/cli.rb CHANGED
@@ -47,6 +47,7 @@ module Tapioca
47
47
  desc "todo", "generate the list of unresolved constants"
48
48
  option :todo_file,
49
49
  type: :string,
50
+ desc: "Path to the generated todo RBI file",
50
51
  default: DEFAULT_TODO_FILE
51
52
  option :file_header,
52
53
  type: :boolean,
@@ -62,26 +62,32 @@ module Tapioca
62
62
 
63
63
  sig { params(constant: ::ActiveModel::Attributes::ClassMethods).returns(T::Array[[::String, ::String]]) }
64
64
  def attribute_methods_for(constant)
65
- constant.attribute_method_matchers.flat_map do |matcher|
65
+ patterns = if constant.respond_to?(:attribute_method_patterns)
66
+ # https://github.com/rails/rails/pull/44367
67
+ T.unsafe(constant).attribute_method_patterns
68
+ else
69
+ constant.attribute_method_matchers
70
+ end
71
+ patterns.flat_map do |pattern|
66
72
  constant.attribute_types.map do |name, value|
67
- next unless handle_method_matcher?(matcher)
73
+ next unless handle_method_pattern?(pattern)
68
74
 
69
- [matcher.method_name(name), type_for(value)]
75
+ [pattern.method_name(name), type_for(value)]
70
76
  end.compact
71
77
  end
72
78
  end
73
79
 
74
- sig do
75
- params(matcher: ::ActiveModel::AttributeMethods::ClassMethods::AttributeMethodMatcher)
76
- .returns(T::Boolean)
77
- end
78
- def handle_method_matcher?(matcher)
79
- target = if matcher.respond_to?(:method_missing_target)
80
+ sig { params(pattern: T.untyped).returns(T::Boolean) }
81
+ def handle_method_pattern?(pattern)
82
+ target = if pattern.respond_to?(:method_missing_target)
80
83
  # Pre-Rails 6.0, the field is named "method_missing_target"
81
- T.unsafe(matcher).method_missing_target
82
- else
84
+ T.unsafe(pattern).method_missing_target
85
+ elsif pattern.respond_to?(:target)
83
86
  # Rails 6.0+ has renamed the field to "target"
84
- matcher.target
87
+ pattern.target
88
+ else
89
+ # https://github.com/rails/rails/pull/44367/files
90
+ T.unsafe(pattern).proxy_target
85
91
  end
86
92
 
87
93
  HANDLED_METHOD_TARGETS.include?(target.to_s)
@@ -109,7 +115,7 @@ module Tapioca
109
115
  return "T.untyped"
110
116
  end
111
117
 
112
- "T.nilable(#{type})"
118
+ as_nilable_type(type)
113
119
  end
114
120
 
115
121
  sig { params(klass: RBI::Scope, method: String, type: String).void }
@@ -184,7 +184,7 @@ module Tapioca
184
184
  end
185
185
  def populate_single_assoc_getter_setter(klass, constant, association_name, reflection)
186
186
  association_class = type_for(constant, reflection)
187
- association_type = "T.nilable(#{association_class})"
187
+ association_type = as_nilable_type(association_class)
188
188
 
189
189
  klass.create_method(
190
190
  association_name.to_s,
@@ -114,8 +114,14 @@ module Tapioca
114
114
  constant.attribute_aliases.each do |attribute_name, column_name|
115
115
  attribute_name = attribute_name.to_s
116
116
  column_name = column_name.to_s
117
- new_method_names = constant.attribute_method_matchers.map { |m| m.method_name(attribute_name) }
118
- old_method_names = constant.attribute_method_matchers.map { |m| m.method_name(column_name) }
117
+ patterns = if constant.respond_to?(:attribute_method_patterns)
118
+ # https://github.com/rails/rails/pull/44367
119
+ T.unsafe(constant).attribute_method_patterns
120
+ else
121
+ constant.attribute_method_matchers
122
+ end
123
+ new_method_names = patterns.map { |m| m.method_name(attribute_name) }
124
+ old_method_names = patterns.map { |m| m.method_name(column_name) }
119
125
  methods_to_add = new_method_names - old_method_names
120
126
 
121
127
  add_methods_for_attribute(mod, constant, column_name, attribute_name, methods_to_add)
@@ -293,12 +299,6 @@ module Tapioca
293
299
  return_type: "T::Boolean"
294
300
  )
295
301
  end
296
-
297
- sig { params(type: String).returns(String) }
298
- def as_nilable_type(type)
299
- return type if type.start_with?("T.nilable(")
300
- "T.nilable(#{type})"
301
- end
302
302
  end
303
303
  end
304
304
  end
@@ -250,6 +250,15 @@ module Tapioca
250
250
  sig { returns(String) }
251
251
  attr_reader :constant_name
252
252
 
253
+ sig { params(type: String).returns(String) }
254
+ def as_nilable_type(type)
255
+ if type.start_with?("T.nilable(", "::T.nilable(") || type == "T.untyped" || type == "::T.untyped"
256
+ type
257
+ else
258
+ "T.nilable(#{type})"
259
+ end
260
+ end
261
+
253
262
  sig { void }
254
263
  def create_classes_and_includes
255
264
  model.create_extend(CommonRelationMethodsModuleName)
@@ -532,7 +541,7 @@ module Tapioca
532
541
  ],
533
542
  return_type: "T::Boolean"
534
543
  )
535
- when :find, :find_by!
544
+ when :find
536
545
  create_common_method(
537
546
  "find",
538
547
  parameters: [
@@ -546,7 +555,15 @@ module Tapioca
546
555
  parameters: [
547
556
  create_rest_param("args", type: "T.untyped"),
548
557
  ],
549
- return_type: "T.nilable(#{constant_name})"
558
+ return_type: as_nilable_type(constant_name)
559
+ )
560
+ when :find_by!
561
+ create_common_method(
562
+ "find_by!",
563
+ parameters: [
564
+ create_rest_param("args", type: "T.untyped"),
565
+ ],
566
+ return_type: constant_name
550
567
  )
551
568
  when :first, :last, :take
552
569
  create_common_method(
@@ -562,7 +579,7 @@ module Tapioca
562
579
  return_type = if method_name.end_with?("!")
563
580
  constant_name
564
581
  else
565
- "T.nilable(#{constant_name})"
582
+ as_nilable_type(constant_name)
566
583
  end
567
584
 
568
585
  create_common_method(
@@ -107,7 +107,7 @@ module Tapioca
107
107
  store_data.accessors.each do |accessor|
108
108
  field = store_data.fields[accessor]
109
109
  type = type_for(field.type_sym)
110
- type = "T.nilable(#{type})" if field.null && type != "T.untyped"
110
+ type = as_nilable_type(type) if field.null
111
111
 
112
112
  store_accessors_module = model.create_module("StoreAccessors")
113
113
  generate_methods(store_accessors_module, field.name.to_s, type)
@@ -136,11 +136,13 @@ module Tapioca
136
136
  method_def = signature.nil? ? method_def : signature.method
137
137
  method_types = parameters_types_from_signature(method_def, signature)
138
138
 
139
- method_def.parameters.each_with_index.map do |(type, name), index|
139
+ parameters = T.let(method_def.parameters, T::Array[[Symbol, T.nilable(Symbol)]])
140
+
141
+ parameters.each_with_index.map do |(type, name), index|
140
142
  fallback_arg_name = "_arg#{index}"
141
143
 
142
- name ||= fallback_arg_name
143
- name = name.to_s.gsub(/&|\*/, fallback_arg_name) # avoid incorrect names from `delegate`
144
+ name = name ? name.to_s : fallback_arg_name
145
+ name = fallback_arg_name unless valid_parameter_name?(name)
144
146
  method_type = T.must(method_types[index])
145
147
 
146
148
  case type
@@ -173,6 +175,20 @@ module Tapioca
173
175
  return_type = "T.untyped" if return_type == "<NOT-TYPED>"
174
176
  return_type
175
177
  end
178
+
179
+ sig { params(type: String).returns(String) }
180
+ def as_nilable_type(type)
181
+ if type.start_with?("T.nilable(", "::T.nilable(") || type == "T.untyped" || type == "::T.untyped"
182
+ type
183
+ else
184
+ "T.nilable(#{type})"
185
+ end
186
+ end
187
+
188
+ sig { params(name: String).returns(T::Boolean) }
189
+ def valid_parameter_name?(name)
190
+ name.match?(/^[[[:alnum:]]_]+$/)
191
+ end
176
192
  end
177
193
  end
178
194
  end
@@ -116,7 +116,7 @@ module Tapioca
116
116
  if returns_collection
117
117
  COLLECTION_TYPE.call(cache_type)
118
118
  else
119
- "T.nilable(::#{cache_type})"
119
+ as_nilable_type(T.must(qualified_name_of(cache_type)))
120
120
  end
121
121
  rescue ArgumentError
122
122
  "T.untyped"
@@ -175,18 +175,20 @@ module Tapioca
175
175
  parameters << create_kw_opt_param("includes", default: "nil", type: "T.untyped")
176
176
 
177
177
  if field.unique
178
+ type = T.must(qualified_name_of(constant))
179
+
178
180
  klass.create_method(
179
181
  "#{name}!",
180
182
  class_method: true,
181
183
  parameters: parameters,
182
- return_type: "::#{constant}"
184
+ return_type: type
183
185
  )
184
186
 
185
187
  klass.create_method(
186
188
  name,
187
189
  class_method: true,
188
190
  parameters: parameters,
189
- return_type: "T.nilable(::#{constant})"
191
+ return_type: as_nilable_type(type)
190
192
  )
191
193
  else
192
194
  klass.create_method(
@@ -64,12 +64,12 @@ module Tapioca
64
64
 
65
65
  sig { override.returns(T::Enumerable[Module]) }
66
66
  def gather_constants
67
- all_modules.select do |const|
67
+ all_classes.select do |const|
68
68
  name = qualified_name_of(const)
69
69
 
70
70
  name &&
71
71
  !name.match?(BUILT_IN_MATCHER) &&
72
- const < ::Rails::Generators::Base
72
+ ::Rails::Generators::Base > const
73
73
  end
74
74
  end
75
75
 
@@ -111,7 +111,7 @@ module Tapioca
111
111
  if arg.required || arg.default
112
112
  type
113
113
  else
114
- "T.nilable(#{type})"
114
+ as_nilable_type(type)
115
115
  end
116
116
  end
117
117
  end
@@ -71,14 +71,15 @@ module Tapioca
71
71
  )
72
72
  return if properties.keys.empty?
73
73
 
74
- instance_methods = constant.instance_methods(false).map(&:to_s).to_set
75
-
76
74
  root.create_path(constant) do |k|
77
- properties.values.each do |property|
78
- generate_methods_for_property(k, property) do |method_name|
79
- !instance_methods.include?(method_name.to_sym)
75
+ smart_properties_methods_name = "SmartPropertiesGeneratedMethods"
76
+ k.create_module(smart_properties_methods_name) do |mod|
77
+ properties.values.each do |property|
78
+ generate_methods_for_property(mod, property)
80
79
  end
81
80
  end
81
+
82
+ k.create_include(smart_properties_methods_name)
82
83
  end
83
84
  end
84
85
 
@@ -95,26 +96,21 @@ module Tapioca
95
96
 
96
97
  sig do
97
98
  params(
98
- klass: RBI::Scope,
99
- property: ::SmartProperties::Property,
100
- block: T.proc.params(arg: String).returns(T::Boolean)
99
+ mod: RBI::Scope,
100
+ property: ::SmartProperties::Property
101
101
  ).void
102
102
  end
103
- def generate_methods_for_property(klass, property, &block)
103
+ def generate_methods_for_property(mod, property)
104
104
  type = type_for(property)
105
105
 
106
106
  if property.writable?
107
107
  name = property.name.to_s
108
108
  method_name = "#{name}="
109
109
 
110
- klass.create_method(
111
- method_name,
112
- parameters: [create_param(name, type: type)],
113
- return_type: type
114
- ) if block.call(method_name)
110
+ mod.create_method(method_name, parameters: [create_param(name, type: type)], return_type: type)
115
111
  end
116
112
 
117
- klass.create_method(property.reader.to_s, return_type: type) if block.call(property.reader.to_s)
113
+ mod.create_method(property.reader.to_s, return_type: type)
118
114
  end
119
115
 
120
116
  BOOLEANS = T.let([
@@ -147,11 +143,8 @@ module Tapioca
147
143
  "T.untyped"
148
144
  end
149
145
 
150
- # Early return for "T.untyped", nothing more to do.
151
- return type if type == "T.untyped"
152
-
153
146
  might_be_optional = Proc === required || !required
154
- type = "T.nilable(#{type})" if might_be_optional
147
+ type = as_nilable_type(type) if might_be_optional
155
148
 
156
149
  type
157
150
  end
@@ -151,9 +151,14 @@ module Tapioca
151
151
 
152
152
  sig { params(mod: Module, helper: Module).returns(T::Boolean) }
153
153
  def includes_helper?(mod, helper)
154
- superclass_ancestors = mod.superclass&.ancestors if Class === mod
155
- superclass_ancestors ||= []
156
- (mod.ancestors - superclass_ancestors).include?(helper)
154
+ superclass_ancestors = []
155
+
156
+ if Class === mod
157
+ superclass = superclass_of(mod)
158
+ superclass_ancestors = ancestors_of(superclass) if superclass
159
+ end
160
+
161
+ (ancestors_of(mod) - superclass_ancestors).any? { |ancestor| helper == ancestor }
157
162
  end
158
163
  end
159
164
  end
@@ -174,20 +174,31 @@ class DynamicMixinCompiler
174
174
 
175
175
  sig { params(tree: RBI::Tree).returns([T::Array[Module], T::Array[Module]]) }
176
176
  def compile_mixes_in_class_methods(tree)
177
- includes = dynamic_includes.select { |mod| (name = name_of(mod)) && !name.start_with?("T::") }
178
- includes.each do |mod|
177
+ includes = dynamic_includes.map do |mod|
179
178
  qname = qualified_name_of(mod)
180
- tree << RBI::Include.new(T.must(qname))
181
- end
179
+
180
+ next if qname.nil? || qname.empty?
181
+ next if filtered_mixin?(qname)
182
+
183
+ tree << RBI::Include.new(qname)
184
+
185
+ mod
186
+ end.compact
182
187
 
183
188
  # If we can generate multiple mixes_in_class_methods, then we want to use all dynamic extends that are not the
184
189
  # constant itself
185
- mixed_in_class_methods = dynamic_extends.select { |mod| mod != @constant }
190
+ mixed_in_class_methods = dynamic_extends.select do |mod|
191
+ mod != @constant && !module_included_by_another_dynamic_extend?(mod, dynamic_extends)
192
+ end
193
+
186
194
  return [[], []] if mixed_in_class_methods.empty?
187
195
 
188
196
  mixed_in_class_methods.each do |mod|
189
197
  qualified_name = qualified_name_of(mod)
198
+
190
199
  next if qualified_name.nil? || qualified_name.empty?
200
+ next if filtered_mixin?(qualified_name)
201
+
191
202
  tree << RBI::MixesInClassMethods.new(qualified_name)
192
203
  end
193
204
 
@@ -195,4 +206,18 @@ class DynamicMixinCompiler
195
206
  rescue
196
207
  [[], []] # silence errors
197
208
  end
209
+
210
+ sig { params(mod: Module, dynamic_extends: T::Array[Module]).returns(T::Boolean) }
211
+ def module_included_by_another_dynamic_extend?(mod, dynamic_extends)
212
+ dynamic_extends.any? do |dynamic_extend|
213
+ mod != dynamic_extend && ancestors_of(dynamic_extend).include?(mod)
214
+ end
215
+ end
216
+
217
+ sig { params(qualified_mixin_name: String).returns(T::Boolean) }
218
+ def filtered_mixin?(qualified_mixin_name)
219
+ # filter T:: namespace mixins that aren't T::Props
220
+ # T::Props and subconstants have semantic value
221
+ qualified_mixin_name.start_with?("::T::") && !qualified_mixin_name.start_with?("::T::Props")
222
+ end
198
223
  end
@@ -17,10 +17,9 @@ module Tapioca
17
17
  def compile
18
18
  config = Spoom::Sorbet::Config.parse_file(@sorbet_path)
19
19
  files = collect_files(config)
20
+ names_in_project = files.map { |file| [File.basename(file, ".rb"), true] }.to_h
20
21
  files.flat_map do |file|
21
- collect_requires(file).reject do |req|
22
- name_in_project?(files, req)
23
- end
22
+ collect_requires(file).reject { |req| names_in_project[req] }
24
23
  end.sort.uniq.map do |name|
25
24
  "require \"#{name}\"\n"
26
25
  end.join
@@ -45,9 +44,10 @@ module Tapioca
45
44
 
46
45
  sig { params(file_path: String).returns(T::Enumerable[String]) }
47
46
  def collect_requires(file_path)
48
- File.read(file_path).lines.map do |line|
47
+ File.binread(file_path).lines.map do |line|
49
48
  /^\s*require\s*(\(\s*)?['"](?<name>[^'"]+)['"](\s*\))?/.match(line) { |m| m["name"] }
50
49
  end.compact
50
+ .reject { |require| require.include?('#{') } # ignore interpolation
51
51
  end
52
52
 
53
53
  sig { params(config: Spoom::Sorbet::Config, file_path: Pathname).returns(T::Boolean) }
@@ -83,13 +83,6 @@ module Tapioca
83
83
  def path_parts(path)
84
84
  T.unsafe(path).descend.map { |part| part.basename.to_s }
85
85
  end
86
-
87
- sig { params(files: T::Enumerable[String], name: String).returns(T::Boolean) }
88
- def name_in_project?(files, name)
89
- files.any? do |file|
90
- File.basename(file, ".rb") == name
91
- end
92
- end
93
86
  end
94
87
  end
95
88
  end