tapioca 0.6.0 → 0.6.4

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.
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