tapioca 0.0.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 30b2f07033fc6b668d5f04c1a9dc33f6d06f776610b4e7051d89f8916695cb3f
4
+ data.tar.gz: 4b06d393137b287fd2b4c0a68955542ad9d9a3ab6be094c144ccc433b787c938
5
+ SHA512:
6
+ metadata.gz: 886c53802ecd357a27d13b045cae53ac2c14dd90e3003d0142f33e410d911f25fc882587e8e19fd44b3d80e03a08eaaa665657943936c6c154c49e44edb4e822
7
+ data.tar.gz: 3183720c7eaa9e89fdda702cbdaa1872eb7ae4ddf0b9b6ba329844a0db6ff29df7adaf2da33c2a92be73605b74eec7c639fa1dbf529f15a86d95d6ae26053bbf
data/Gemfile CHANGED
@@ -1,4 +1,13 @@
1
- source 'https://rubygems.org'
1
+ # frozen_string_literal: true
2
+
3
+ source("https://rubygems.org")
2
4
 
3
- # Specify your gem's dependencies in tapioca.gemspec
4
5
  gemspec
6
+
7
+ group(:deployment) do
8
+ gem("package_cloud", "~> 0.2.33")
9
+ end
10
+
11
+ group(:deployment, :development) do
12
+ gem("rake", "~> 12.3")
13
+ end
data/README.md CHANGED
@@ -1,17 +1,61 @@
1
1
  # Tapioca
2
2
 
3
- One project to glue them all
3
+ [![Build Status](https://travis-ci.com/Shopify/tapioca.svg?token=AuiMGLmuYDrK2mb81pyq&branch=master)](https://travis-ci.com/Shopify/tapioca)
4
+
5
+ Tapioca is a library used to generate RBI (Ruby interface) files for use with [Sorbet](https://sorbet.org). RBI files provide the structure (classes, modules, methods, parameters) of the gem/library to Sorbet to assist with typechecking.
4
6
 
5
7
  ## Installation
6
8
 
7
- Add this line to your application's Gemfile:
9
+ Add this line to your application's `Gemfile`:
10
+
11
+ ```ruby
12
+ gem 'tapioca', '~> 0.1.2', require: false
13
+ ```
14
+
15
+ and do not forget to execute `tapioca` using `bundler`:
16
+
17
+ ```shell
18
+ $ bundle exec tapioca
19
+ Commands:
20
+ tapioca bundle # sync RBIs to Gemfile
21
+ tapioca generate [gem...] # generate RBIs from gems
22
+ tapioca help [COMMAND] # Describe available commands or one specific command
23
+
24
+ Options:
25
+ --pre, -b, [--prerequire=file] # A file to be required before Bundler.require is called
26
+ --post, -a, [--postrequire=file] # A file to be required after Bundler.require is called
27
+ --out, -o, [--outdir=directory] # The output directory for generated RBI files
28
+ # Default: sorbet/rbi/gems
29
+ --cmd, -c, [--generate-command=command] # The command to run to regenerate RBI files
30
+ --typed, -t, [--typed-overrides=gem:level] # Overrides for typed sigils for generated gem RBIs
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ### Generate for gems
36
+
37
+ Command: `tapioca generate [gems...]`
38
+
39
+ This will generate RBIs for the specified gems and place them in the RBI directory.
40
+
41
+ ### Generate for all gems in Gemfile
42
+
43
+ Command: `tapioca bundle`
44
+
45
+ This will sync the RBIs with the gems in the Gemfile and will add, update, and remove RBIs as necessary.
46
+
47
+ ### Flags
8
48
 
9
- gem 'tapioca'
49
+ - `--prerequire [file]`: A file to be required before `Bundler.require` is called.
50
+ - `--postrequire [file]`: A file to be required after `Bundler.require` is called.
51
+ - `--out [directory]`: The output directory for generated RBI files, default to `sorbet/rbi/gems`.
52
+ - `--generate_command [command]`: The command to run to regenerate RBI files (used in header comment of the RBI files), defaults to the current command.
53
+ - `--typed_overrides [gem:level]`: Overrides typed sigils for generated gem RBIs for gem `gem` to level `level` (`level` can be one of `ignore`, `false`, `true`, `strict`, or `strong`, see [the Sorbet docs](https://sorbet.org/docs/static#file-level-granularity-strictness-levels) for more details).
10
54
 
11
- And then execute:
55
+ ## Contributing
12
56
 
13
- $ bundle
57
+ 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](http://contributor-covenant.org) code of conduct.
14
58
 
15
- Or install it yourself as:
59
+ ## License
16
60
 
17
- $ gem install tapioca
61
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -1,2 +1,12 @@
1
- #!/usr/bin/env rake
1
+ # frozen_string_literal: true
2
+
2
3
  require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ begin
7
+ require 'rspec/core/rake_task'
8
+ RSpec::Core::RakeTask.new(:spec)
9
+ rescue LoadError # rubocop:disable Lint/HandleExceptions
10
+ end
11
+
12
+ task(default: :spec)
@@ -0,0 +1,6 @@
1
+ #! /usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/tapioca"
5
+
6
+ Tapioca::Cli.start(ARGV)
@@ -0,0 +1,50 @@
1
+ unless defined?(T)
2
+ module T
3
+ def self.any(type_a, type_b, *types); end
4
+ def self.nilable(type); end
5
+ def self.untyped; end
6
+ def self.noreturn; end
7
+ def self.all(type_a, type_b, *types); end
8
+ def self.enum(values); end
9
+ def self.proc; end
10
+ def self.self_type; end
11
+ def self.class_of(klass); end
12
+ def self.type_alias(type); end
13
+ def self.type_parameter(name); end
14
+
15
+ def self.cast(value, type, checked: true); value; end
16
+ def self.let(value, type, checked: true); value; end
17
+ def self.assert_type!(value, type, checked: true); value; end
18
+ def self.unsafe(value); value; end
19
+ def self.must(arg, msg=nil); arg; end
20
+ def self.reveal_type(value); value; end
21
+ end
22
+
23
+ module T::Sig
24
+ def sig(&blk); end
25
+ end
26
+
27
+ module T::Array
28
+ def self.[](type); end
29
+ end
30
+
31
+ module T::Hash
32
+ def self.[](keys, values); end
33
+ end
34
+
35
+ module T::Enumerable
36
+ def self.[](type); end
37
+ end
38
+
39
+ module T::Range
40
+ def self.[](type); end
41
+ end
42
+
43
+ module T::Set
44
+ def self.[](type); end
45
+ end
46
+
47
+ module T::Boolean
48
+ end
49
+ end
50
+
@@ -1,4 +1,21 @@
1
- require "tapioca/version"
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "zeitwerk"
5
+ require_relative "./t"
6
+
7
+ loader = Zeitwerk::Loader.for_gem
8
+ loader.setup
9
+ loader.eager_load
2
10
 
3
11
  module Tapioca
12
+ def self.silence_warnings
13
+ original_verbosity = $VERBOSE
14
+ $VERBOSE = nil
15
+ yield
16
+ ensure
17
+ $VERBOSE = original_verbosity
18
+ end
19
+
20
+ class Error < StandardError; end
4
21
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ require 'thor'
5
+
6
+ module Tapioca
7
+ class Cli < Thor
8
+ class_option :prerequire,
9
+ aliases: ["--pre", "-b"],
10
+ banner: "file",
11
+ desc: "A file to be required before Bundler.require is called"
12
+ class_option :postrequire,
13
+ aliases: ["--post", "-a"],
14
+ banner: "file",
15
+ desc: "A file to be required after Bundler.require is called"
16
+ class_option :outdir,
17
+ aliases: ["--out", "-o"],
18
+ default: Generator::DEFAULT_OUTDIR,
19
+ banner: "directory",
20
+ desc: "The output directory for generated RBI files"
21
+ class_option :generate_command,
22
+ aliases: ["--cmd", "-c"],
23
+ banner: "command",
24
+ desc: "The command to run to regenerate RBI files"
25
+ class_option :typed_overrides,
26
+ aliases: ["--typed", "-t"],
27
+ type: :hash,
28
+ default: {},
29
+ banner: "gem:level",
30
+ desc: "Overrides for typed sigils for generated gem RBIs"
31
+
32
+ desc "generate [gem...]", "generate RBIs from gems"
33
+ def generate(*gems)
34
+ Tapioca.silence_warnings do
35
+ generator.build_gem_rbis(gems)
36
+ end
37
+ end
38
+
39
+ desc "bundle", "sync RBIs to Gemfile"
40
+ def bundle
41
+ Tapioca.silence_warnings do
42
+ generator.sync_rbis_with_gemfile
43
+ end
44
+ end
45
+
46
+ no_commands do
47
+ def generator
48
+ @generator ||= Generator.new(
49
+ outdir: options[:outdir],
50
+ prerequire: options[:prerequire],
51
+ postrequire: options[:postrequire],
52
+ command: options[:generate_command],
53
+ typed_overrides: options[:typed_overrides]
54
+ )
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,571 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ require 'pathname'
5
+
6
+ module Tapioca
7
+ module Compilers
8
+ module SymbolTable
9
+ class SymbolGenerator
10
+ extend(T::Sig)
11
+
12
+ IGNORED_SYMBOLS = %w{
13
+ YAML
14
+ MiniTest
15
+ Mutex
16
+ }
17
+
18
+ attr_reader(:gem, :indent)
19
+
20
+ sig { params(gem: Gemfile::Gem, indent: Integer).void }
21
+ def initialize(gem, indent = 0)
22
+ @gem = gem
23
+ @indent = indent
24
+ @seen = Set.new
25
+ @alias_namespace ||= Set.new
26
+ end
27
+
28
+ sig { returns(String) }
29
+ def generate
30
+ symbols
31
+ .sort
32
+ .map(&method(:generate_from_symbol))
33
+ .compact
34
+ .join("\n\n")
35
+ .concat("\n")
36
+ end
37
+
38
+ private
39
+
40
+ sig { returns(T::Set[String]) }
41
+ def symbols
42
+ symbols = Tapioca::Compilers::SymbolTable::SymbolLoader.list_from_paths(gem.files)
43
+ symbols.union(engine_symbols(symbols))
44
+ end
45
+
46
+ sig { params(symbols: T::Set[String]).returns(T::Set[String]) }
47
+ def engine_symbols(symbols)
48
+ return Set.new unless Object.const_defined?("Rails::Engine")
49
+
50
+ engine = Object.const_get("Rails::Engine")
51
+ .descendants.reject(&:abstract_railtie?)
52
+ .find do |klass|
53
+ name = name_of(klass)
54
+ !name.nil? && symbols.include?(name)
55
+ end
56
+
57
+ return Set.new unless engine
58
+
59
+ paths = engine.config.eager_load_paths.flat_map do |load_path|
60
+ Pathname.glob("#{load_path}/**/*.rb")
61
+ end
62
+
63
+ Tapioca::Compilers::SymbolTable::SymbolLoader.list_from_paths(paths)
64
+ rescue
65
+ Set.new
66
+ end
67
+
68
+ sig { params(symbol: String).returns(T.nilable(String)) }
69
+ def generate_from_symbol(symbol)
70
+ constant = resolve_constant(symbol)
71
+
72
+ return unless constant
73
+
74
+ compile(symbol, constant)
75
+ end
76
+
77
+ sig { params(symbol: String).returns(BasicObject) }
78
+ def resolve_constant(symbol)
79
+ Object.const_get(symbol, false)
80
+ rescue NameError, LoadError, RuntimeError, ArgumentError, TypeError
81
+ nil
82
+ end
83
+
84
+ sig { params(name: T.nilable(String), constant: BasicObject).returns(T.nilable(String)) }
85
+ def compile(name, constant)
86
+ return unless constant
87
+ return unless name
88
+ return if name.strip.empty?
89
+ return if name.start_with?('#<')
90
+ return if name.downcase == name
91
+ return if alias_namespaced?(name)
92
+ return if seen?(name)
93
+ return unless parent_declares_constant?(name)
94
+
95
+ mark_seen(name)
96
+ compile_constant(name, constant)
97
+ end
98
+
99
+ sig { params(name: String, constant: BasicObject).returns(T.nilable(String)) }
100
+ def compile_constant(name, constant)
101
+ case constant
102
+ when Module
103
+ if name_of(constant) != name
104
+ compile_alias(name, constant)
105
+ else
106
+ compile_module(name, constant)
107
+ end
108
+ else
109
+ compile_object(name, constant)
110
+ end
111
+ end
112
+
113
+ sig { params(name: String, constant: Module).returns(T.nilable(String)) }
114
+ def compile_alias(name, constant)
115
+ return if symbol_ignored?(name)
116
+
117
+ constant_name = name_of(constant)
118
+ add_to_alias_namespace(name)
119
+
120
+ return if IGNORED_SYMBOLS.include?(name)
121
+
122
+ indented("#{name} = #{constant_name}")
123
+ end
124
+
125
+ sig { params(name: String, value: BasicObject).returns(T.nilable(String)) }
126
+ def compile_object(name, value)
127
+ indented("#{name} = T.let(T.unsafe(nil), #{type_name_of(value)})")
128
+ end
129
+
130
+ sig { params(value: BasicObject).returns(String) }
131
+ def type_name_of(value)
132
+ klass = class_of(value)
133
+
134
+ type_name = public_module?(klass) && name_of(klass) || "T.untyped"
135
+ # Range needs to be processed separately to be put in the T::Range[] form
136
+ type_name = "T::Range[#{type_name_of(T.cast(value, T::Range[T.untyped]).first)}]" if klass == Range
137
+
138
+ type_name
139
+ end
140
+
141
+ sig { params(name: String, constant: Module).returns(T.nilable(String)) }
142
+ def compile_module(name, constant)
143
+ return unless public_module?(constant)
144
+ return unless defined_in_gem?(constant, strict: false)
145
+
146
+ header =
147
+ if constant.is_a?(Class)
148
+ indented("class #{name}#{compile_superclass(constant)}")
149
+ else
150
+ indented("module #{name}")
151
+ end
152
+
153
+ body = compile_body(name, constant)
154
+
155
+ return if symbol_ignored?(name) && body.nil?
156
+
157
+ [
158
+ header,
159
+ body,
160
+ indented("end"),
161
+ compile_subconstants(name, constant),
162
+ ].select { |b| !b.nil? && b.strip != "" }.join("\n")
163
+ end
164
+
165
+ sig { params(name: String, constant: Module).returns(T.nilable(String)) }
166
+ def compile_body(name, constant)
167
+ with_indentation do
168
+ methods = compile_methods(name, constant)
169
+
170
+ return if symbol_ignored?(name) && methods.nil?
171
+
172
+ [
173
+ compile_mixins(constant),
174
+ methods,
175
+ ].select { |b| b != "" }.join("\n\n")
176
+ end
177
+ end
178
+
179
+ sig { params(name: String, constant: Module).returns(T.nilable(String)) }
180
+ def compile_subconstants(name, constant)
181
+ output = constants_of(constant).sort.uniq.map do |constant_name|
182
+ symbol = (name == "Object" ? "" : name) + "::#{constant_name}"
183
+ subconstant = resolve_constant(symbol)
184
+
185
+ # Don't compile modules of Object because Object::Foo == Foo
186
+ # Don't compile modules of BasicObject because BasicObject::BasicObject == BasicObject
187
+ next if (Object == constant || BasicObject == constant) && Module === subconstant
188
+ next unless subconstant
189
+
190
+ compile(symbol, subconstant)
191
+ end.compact
192
+
193
+ return "" if output.empty?
194
+
195
+ "\n" + output.join("\n\n")
196
+ end
197
+
198
+ sig { params(constant: Class).returns(String) }
199
+ def compile_superclass(constant)
200
+ superclass = T.let(nil, T.nilable(Class)) # rubocop:disable Lint/UselessAssignment
201
+
202
+ while (superclass = superclass_of(constant))
203
+ constant_name = name_of(constant)
204
+ constant = superclass
205
+
206
+ # Some classes have superclasses that are private constants
207
+ # so if we generate code with that superclass, the output
208
+ # will not be compilable (since private constants are not
209
+ # publicly visible).
210
+ #
211
+ # So we skip superclasses that are not public and walk up the
212
+ # chain.
213
+ next unless public_module?(superclass)
214
+
215
+ # Some types have "themselves" as their superclass
216
+ # which can happen via:
217
+ #
218
+ # class A < Numeric; end
219
+ # A = Class.new(A)
220
+ # A.superclass #=> A
221
+ #
222
+ # We compare names here to make sure we skip those
223
+ # superclass instances and walk up the chain.
224
+ #
225
+ # The name comparison is against the name of the constant
226
+ # resolved from the name of the superclass, since
227
+ # this is also possible:
228
+ #
229
+ # B = Class.new
230
+ # class A < B; end
231
+ # B = A
232
+ # A.superclass.name #=> "B"
233
+ # B #=> A
234
+ superclass_name = T.must(name_of(superclass))
235
+ resolved_superclass = resolve_constant(superclass_name)
236
+ next unless Module === resolved_superclass
237
+ next if name_of(resolved_superclass) == constant_name
238
+
239
+ # We found a suitable superclass
240
+ break
241
+ end
242
+
243
+ return "" if superclass == ::Object || superclass == ::Delegator
244
+ return "" if superclass.nil?
245
+
246
+ name = name_of(superclass)
247
+ return "" if name.nil? || name.empty?
248
+
249
+ " < ::#{name}"
250
+ end
251
+
252
+ sig { params(constant: Module).returns(String) }
253
+ def compile_mixins(constant)
254
+ ignorable_ancestors =
255
+ if constant.is_a?(Class)
256
+ ancestors = constant.superclass&.ancestors || Object.ancestors
257
+ Set.new(ancestors)
258
+ else
259
+ Module.ancestors
260
+ end
261
+
262
+ inherited_singleton_class_ancestors =
263
+ if constant.is_a?(Class)
264
+ Set.new(constant.superclass.singleton_class.ancestors)
265
+ else
266
+ Module.ancestors
267
+ end
268
+
269
+ interesting_ancestors =
270
+ constant.ancestors.reject { |mod| ignorable_ancestors.include?(mod) }
271
+
272
+ prepend = interesting_ancestors.take_while { |c| !are_equal?(constant, c) }
273
+ include = interesting_ancestors.drop(prepend.size + 1)
274
+ extend = constant.singleton_class.ancestors
275
+ .reject do |mod|
276
+ mod == constant.singleton_class ||
277
+ inherited_singleton_class_ancestors.include?(mod) ||
278
+ !public_module?(mod) ||
279
+ Module != class_of(mod)
280
+ end
281
+
282
+ prepends = prepend
283
+ .select(&method(:name_of))
284
+ .select(&method(:public_module?))
285
+ .map do |mod|
286
+ # TODO: Sorbet currently does not handle prepend
287
+ # properly for method resolution, so we generate an
288
+ # include statement instead
289
+ indented("include(#{qualified_name_of(mod)})")
290
+ end
291
+
292
+ includes = include
293
+ .select(&method(:name_of))
294
+ .select(&method(:public_module?))
295
+ .map do |mod|
296
+ indented("include(#{qualified_name_of(mod)})")
297
+ end
298
+
299
+ extends = extend
300
+ .select(&method(:name_of))
301
+ .select(&method(:public_module?))
302
+ .map do |mod|
303
+ indented("extend(#{qualified_name_of(mod)})")
304
+ end
305
+
306
+ mixes_class_methods = extend
307
+ .select do |mod|
308
+ qualified_name_of(mod) == "::ActiveSupport::Concern" &&
309
+ Module === resolve_constant("#{name_of(constant)}::ClassMethods")
310
+ end
311
+ .first(1)
312
+ .flat_map do
313
+ ["", indented("mixes_in_class_methods(ClassMethods)")]
314
+ end
315
+
316
+ (prepends + includes + extends + mixes_class_methods).join("\n")
317
+ end
318
+
319
+ sig { params(name: String, constant: Module).returns(T.nilable(String)) }
320
+ def compile_methods(name, constant)
321
+ initialize_method = compile_method(
322
+ name,
323
+ constant,
324
+ initialize_method_for(constant)
325
+ )
326
+
327
+ instance_methods = compile_directly_owned_methods(name, constant)
328
+ singleton_methods = compile_directly_owned_methods(name, constant.singleton_class, [:public])
329
+
330
+ return if symbol_ignored?(name) && instance_methods.empty? && singleton_methods.empty?
331
+
332
+ [
333
+ initialize_method || "",
334
+ instance_methods,
335
+ singleton_methods,
336
+ ].select { |b| b.strip != "" }.join("\n\n")
337
+ end
338
+
339
+ sig { params(module_name: String, mod: Module, for_visibility: T::Array[Symbol]).returns(String) }
340
+ def compile_directly_owned_methods(module_name, mod, for_visibility = [:public, :protected, :private])
341
+ method_names_by_visibility(mod)
342
+ .delete_if { |visibility, _method_list| !for_visibility.include?(visibility) }
343
+ .flat_map do |visibility, method_list|
344
+ compiled = method_list.sort!.map do |name|
345
+ next if name == :initialize
346
+ compile_method(module_name, mod, mod.instance_method(name))
347
+ end
348
+ compiled.compact!
349
+
350
+ unless compiled.empty? || visibility == :public
351
+ # add visibility badge
352
+ compiled.unshift('', indented(visibility.to_s), '')
353
+ end
354
+
355
+ compiled
356
+ end
357
+ .compact
358
+ .join("\n")
359
+ end
360
+
361
+ sig { params(mod: Module).returns(T::Hash[Symbol, T::Array[Symbol]]) }
362
+ def method_names_by_visibility(mod)
363
+ {
364
+ public: Module.instance_method(:public_instance_methods).bind(mod).call,
365
+ protected: Module.instance_method(:protected_instance_methods).bind(mod).call,
366
+ private: Module.instance_method(:private_instance_methods).bind(mod).call,
367
+ }
368
+ end
369
+
370
+ sig do
371
+ params(
372
+ symbol_name: String,
373
+ constant: Module,
374
+ method: T.nilable(UnboundMethod)
375
+ ).returns(T.nilable(String))
376
+ end
377
+ def compile_method(symbol_name, constant, method)
378
+ return unless method
379
+ return unless method.owner == constant
380
+ return if symbol_ignored?(symbol_name) && !method_in_gem?(method)
381
+
382
+ method_name = method.name.to_s
383
+ return unless valid_method_name?(method_name)
384
+
385
+ params = T.let(method.parameters, T::Array[T::Array[Symbol]])
386
+ parameters = params.map do |(type, name)|
387
+ name ||= :_
388
+
389
+ case type
390
+ when :req
391
+ name.to_s
392
+ when :opt
393
+ "#{name} = _"
394
+ when :rest
395
+ "*#{name}"
396
+ when :keyreq
397
+ "#{name}:"
398
+ when :key
399
+ "#{name}: _"
400
+ when :keyrest
401
+ "**#{name}"
402
+ when :block
403
+ "&#{name}"
404
+ end
405
+ end.join(', ')
406
+
407
+ method_name.prepend(constant.singleton_class? ? 'self.' : '')
408
+ parameters = "(#{parameters})" if parameters != ""
409
+
410
+ indented("def #{method_name}#{parameters}; end")
411
+ end
412
+
413
+ sig { params(symbol_name: String).returns(T::Boolean) }
414
+ def symbol_ignored?(symbol_name)
415
+ SymbolLoader.ignore_symbol?(symbol_name)
416
+ end
417
+
418
+ sig { params(path: String).returns(T::Boolean) }
419
+ def path_in_gem?(path)
420
+ path.start_with?(gem.full_gem_path)
421
+ end
422
+
423
+ SPECIAL_METHOD_NAMES = %w[! ~ +@ ** -@ * / % + - << >> & | ^ < <= => > >= == === != =~ !~ <=> [] []= `]
424
+
425
+ sig { params(name: String).returns(T::Boolean) }
426
+ def valid_method_name?(name)
427
+ return true if SPECIAL_METHOD_NAMES.include?(name)
428
+ !!name.match(/^[[:word:]]+[?!=]?$/)
429
+ end
430
+
431
+ sig do
432
+ type_parameters(:U)
433
+ .params(
434
+ _blk: T.proc
435
+ .returns(T.type_parameter(:U))
436
+ )
437
+ .returns(T.type_parameter(:U))
438
+ end
439
+ def with_indentation(&_blk)
440
+ @indent += 2
441
+ yield
442
+ ensure
443
+ @indent -= 2
444
+ end
445
+
446
+ sig { params(str: String).returns(String) }
447
+ def indented(str)
448
+ " " * @indent + str
449
+ end
450
+
451
+ sig { params(method: UnboundMethod).returns(T::Boolean) }
452
+ def method_in_gem?(method)
453
+ source_location = method.source_location&.first
454
+ return false if source_location.nil?
455
+
456
+ path_in_gem?(source_location)
457
+ end
458
+
459
+ sig { params(constant: Module, strict: T::Boolean).returns(T::Boolean) }
460
+ def defined_in_gem?(constant, strict: true)
461
+ files = get_file_candidates(constant)
462
+
463
+ return !strict if files.empty?
464
+
465
+ files.any? do |file|
466
+ path_in_gem?(file)
467
+ end
468
+ end
469
+
470
+ sig { params(constant: Module).returns(T::Array[String]) }
471
+ def get_file_candidates(constant)
472
+ wrapped_module = Pry::WrappedModule.new(constant)
473
+
474
+ wrapped_module.candidates.map(&:file).to_a.compact
475
+ rescue ArgumentError, NameError
476
+ []
477
+ end
478
+
479
+ sig { params(name: String).void }
480
+ def add_to_alias_namespace(name)
481
+ @alias_namespace.add("#{name}::")
482
+ end
483
+
484
+ sig { params(name: String).returns(T::Boolean) }
485
+ def alias_namespaced?(name)
486
+ @alias_namespace.any? do |namespace|
487
+ name.start_with?(namespace)
488
+ end
489
+ end
490
+
491
+ sig { params(name: String).void }
492
+ def mark_seen(name)
493
+ @seen.add(name)
494
+ end
495
+
496
+ sig { params(name: String).returns(T::Boolean) }
497
+ def seen?(name)
498
+ @seen.include?(name)
499
+ end
500
+
501
+ def initialize_method_for(constant)
502
+ constant.instance_method(:initialize)
503
+ rescue
504
+ nil
505
+ end
506
+
507
+ def parent_declares_constant?(name)
508
+ name_parts = name.split("::")
509
+
510
+ parent_name = name_parts[0...-1].join("::").delete_prefix("::")
511
+ parent_name = 'Object' if parent_name == ""
512
+ parent = T.cast(resolve_constant(parent_name), T.nilable(Module))
513
+
514
+ return false unless parent
515
+
516
+ constants_of(parent).include?(name_parts.last.to_sym)
517
+ end
518
+
519
+ sig { params(constant: Module).returns(T::Boolean) }
520
+ def public_module?(constant)
521
+ constant_name = name_of(constant)
522
+ return false unless constant_name
523
+
524
+ begin
525
+ # can't use !! here because the constant might override ! and mess with us
526
+ Module === eval(constant_name) # rubocop:disable Security/Eval
527
+ rescue NameError
528
+ false
529
+ end
530
+ end
531
+
532
+ sig { params(constant: BasicObject).returns(Class) }
533
+ def class_of(constant)
534
+ Kernel.instance_method(:class).bind(constant).call
535
+ end
536
+
537
+ sig { params(constant: Module).returns(T::Array[Symbol]) }
538
+ def constants_of(constant)
539
+ Module.instance_method(:constants).bind(constant).call(false)
540
+ end
541
+
542
+ sig { params(constant: Module).returns(T.nilable(String)) }
543
+ def name_of(constant)
544
+ name = Module.instance_method(:name).bind(constant).call
545
+ return if name.nil?
546
+ return unless are_equal?(constant, resolve_constant(name))
547
+ name = "Struct" if name =~ /^(::)?Struct::[^:]+$/
548
+ name
549
+ end
550
+
551
+ sig { params(constant: Module).returns(T.nilable(String)) }
552
+ def qualified_name_of(constant)
553
+ name = name_of(constant)
554
+ return if name.nil?
555
+ name.prepend("::") unless name.start_with?("::")
556
+ name
557
+ end
558
+
559
+ sig { params(constant: Class).returns(T.nilable(Class)) }
560
+ def superclass_of(constant)
561
+ Class.instance_method(:superclass).bind(constant).call
562
+ end
563
+
564
+ sig { params(constant: Module, other: BasicObject).returns(T::Boolean) }
565
+ def are_equal?(constant, other)
566
+ BasicObject.instance_method(:equal?).bind(constant).call(other)
567
+ end
568
+ end
569
+ end
570
+ end
571
+ end