tapioca 0.0.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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