Class: Sorbet::Private::HiddenMethodFinder

Inherits:
Object
  • Object
show all
Includes:
StepInterface
Defined in:
lib/hidden-definition-finder.rb

Constant Summary collapse

PATH =
"sorbet/rbi/hidden-definitions/"
TMP_PATH =
Dir.mktmpdir + "/"
TMP_RBI =
TMP_PATH + "reflection.rbi"
DIFF_RBI =
TMP_PATH + "hidden.rbi.tmp"
RBI_CONSTANTS =
TMP_PATH + "reflection.json"
RBI_CONSTANTS_ERR =
RBI_CONSTANTS + ".err"
SOURCE_CONSTANTS =
TMP_PATH + "from-source.json"
SOURCE_CONSTANTS_ERR =
SOURCE_CONSTANTS + ".err"
HIDDEN_RBI =
PATH + "hidden.rbi"
ERRORS_RBI =
PATH + "errors.txt"
HEADER =
Sorbet::Private::Serialize.header('autogenerated', 'hidden-definitions')
SORTED_SET_AUTOLOAD =
!Object.autoload?(:SortedSet).nil?
DENYLIST =

These methods are defined in C++ and we want our C++ definition to win instead of a shim.

Set.new([
  [Class.object_id, "new"],
  [BasicObject.object_id, "initialize"],
]).freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.mainObject



41
42
43
# File 'lib/hidden-definition-finder.rb', line 41

def self.main
  self.new.main
end

.output_fileObject



460
461
462
# File 'lib/hidden-definition-finder.rb', line 460

def self.output_file
  PATH
end

Instance Method Details

#all_modules_and_aliasesObject



77
78
79
80
# File 'lib/hidden-definition-finder.rb', line 77

def all_modules_and_aliases
  puts "Naming all Modules"
  [constant_cache.all_module_names.sort, constant_cache.all_module_aliases]
end

#capture_stderrObject



412
413
414
415
416
417
418
419
# File 'lib/hidden-definition-finder.rb', line 412

def capture_stderr
  real_stderr = $stderr
  $stderr = StringIO.new
  yield
  $stderr.string
ensure
  $stderr = real_stderr
end

#constant_cacheObject



71
72
73
74
# File 'lib/hidden-definition-finder.rb', line 71

def constant_cache
  @cache ||= Sorbet::Private::ConstantLookupCache.new
  @cache
end

#gen_source_rbi(classes, aliases) ⇒ Object



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/hidden-definition-finder.rb', line 86

def gen_source_rbi(classes, aliases)
  puts "Generating #{TMP_RBI} with #{classes.count} modules and #{aliases.count} aliases"
  serializer = Sorbet::Private::Serialize.new(constant_cache)
  buffer = []
  buffer << Sorbet::Private::Serialize.header

  # should we do something with these errors?
  capture_stderr do
    classes.each do |class_name|
      buffer << serializer.class_or_module(class_name)
    end
    aliases.each do |base, other_names|
      other_names.each do |other_name|
        buffer << serializer.alias(base, other_name)
      end
    end
  end
  File.write(TMP_RBI, buffer.join("\n"))
end

#looks_like_stub_name(name) ⇒ Object



325
326
327
# File 'lib/hidden-definition-finder.rb', line 325

def looks_like_stub_name(name)
  name.include?('$')
end

#mainObject



45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/hidden-definition-finder.rb', line 45

def main
  mk_dir
  require_everything
  classes, aliases = all_modules_and_aliases
  gen_source_rbi(classes, aliases)
  rm_rbis
  write_constants
  source, rbi = read_constants
  write_diff(source, rbi)
  split_rbi
  rm_dir
end

#mk_dirObject



58
59
60
# File 'lib/hidden-definition-finder.rb', line 58

def mk_dir
  FileUtils.mkdir_p(PATH) unless Dir.exist?(PATH)
end

#read_constantsObject



165
166
167
168
169
170
171
# File 'lib/hidden-definition-finder.rb', line 165

def read_constants
  puts "Reading #{SOURCE_CONSTANTS}"
  source = JSON.parse(File.read(SOURCE_CONSTANTS))
  puts "Reading #{RBI_CONSTANTS}"
  rbi = JSON.parse(File.read(RBI_CONSTANTS))
  [source, rbi]
end

#real_name(mod) ⇒ Object



82
83
84
# File 'lib/hidden-definition-finder.rb', line 82

def real_name(mod)
  constant_cache.name_by_class(mod)
end

#require_everythingObject



66
67
68
69
# File 'lib/hidden-definition-finder.rb', line 66

def require_everything
  puts "Requiring all of your code"
  Sorbet::Private::RequireEverything.require_everything
end

#rm_dirObject



62
63
64
# File 'lib/hidden-definition-finder.rb', line 62

def rm_dir
  FileUtils.rm_r(TMP_PATH)
end

#serialize_alias(source_entry, rbi_entry, my_klass, source_symbols, rbi_symbols) ⇒ Object



306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/hidden-definition-finder.rb', line 306

def serialize_alias(source_entry, rbi_entry, my_klass, source_symbols, rbi_symbols)
  return if rbi_entry["kind"] != "STATIC_FIELD"
  return if source_entry == rbi_entry
  if source_entry
    is_stub = source_entry['superClass'] && source_symbols[source_entry['superClass']] == 'Sorbet::Private::Static::StubModule'
    if !is_stub
      return
    end
  end
  return if !rbi_entry["aliasTo"]

  fqn = rbi_symbols[rbi_entry["id"]]
  other_fqn = rbi_symbols[rbi_entry["aliasTo"]]
  return if looks_like_stub_name(fqn)
  ret = String.new
  ret << "#{fqn} = #{other_fqn}\n"
  return ret
end

#serialize_class(source_entry, rbi_entry, klass, source_symbols, rbi_symbols, source_by_name) ⇒ Object



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/hidden-definition-finder.rb', line 223

def serialize_class(source_entry, rbi_entry, klass, source_symbols, rbi_symbols, source_by_name)
  return if rbi_entry["kind"] != "CLASS_OR_MODULE"

  name = rbi_entry["name"]["name"]
  if name.start_with?('<Class:')
    name = name.sub('<Class:', '').sub('>', '')
    my_klass_is_singleton = true
  else
    my_klass_is_singleton = false
  end

  return if SORTED_SET_AUTOLOAD && name == 'SortedSet'

  begin
    my_klass = klass.const_get(name, false) # rubocop:disable PrisonGuard/NoDynamicConstAccess
  rescue LoadError, NameError, ArgumentError => e
    return "# #{e.message.gsub("\n", "\n# ")}"
  end

  return if !Sorbet::Private::RealStdlib.real_is_a?(my_klass, Class) && !Sorbet::Private::RealStdlib.real_is_a?(my_klass, Module)

  # We specifically don't typecheck anything in T:: since it is hardcoded
  # into sorbet. We don't include anything in Sorbet::Private:: because
  # it's private.
  return if ['T', 'Sorbet::Private'].include?(real_name(my_klass))

  source_type = nil
  if !source_entry
    if source_by_name[name]
      source_type = source_by_name[name]["kind"]
    end
  else
    source_type = source_entry["kind"]
  end
  if source_type && source_type != "CLASS_OR_MODULE"
    return "# The source says #{real_name(my_klass)} is a #{source_type} but reflection says it is a #{rbi_entry['kind']}"
  end

  if !source_entry
    source_children = []
    source_mixins = []
    is_stub = true
  else
    source_children = source_entry.fetch("children", [])
    source_mixins = source_entry.fetch("mixins", [])
    is_stub = source_entry['superClass'] && source_symbols[source_entry['superClass']] == 'Sorbet::Private::Static::StubModule'
  end
  rbi_children = rbi_entry.fetch("children", [])
  rbi_mixins = rbi_entry.fetch("mixins", [])

  methods = serialize_methods(source_children, rbi_children, my_klass, my_klass_is_singleton)
  includes = serialize_includes(source_mixins, rbi_mixins, my_klass, my_klass_is_singleton, source_symbols, rbi_symbols)
  values = serialize_values(source_children, rbi_children, my_klass, source_symbols)

  ret = []
  if !without_errors(methods).empty? || !without_errors(includes).empty? || !without_errors(values).empty? || is_stub
    fqn = real_name(my_klass)
    if fqn
      klass_str = String.new
      klass_str << (Sorbet::Private::RealStdlib.real_is_a?(my_klass, Class) ? "class #{fqn}\n" : "module #{fqn}\n")
      klass_str << includes.join("\n")
      klass_str << "\n" unless klass_str.end_with?("\n")
      klass_str << methods.join("\n")
      klass_str << "\n" unless klass_str.end_with?("\n")
      klass_str << values.join("\n")
      klass_str << "\n" unless klass_str.end_with?("\n")
      klass_str << "end\n"
      ret << klass_str
    end
  end

  children = serialize_constants(source_children, rbi_children, my_klass, my_klass_is_singleton, source_symbols, rbi_symbols)
  if children != ""
    ret << children
  end

  ret.empty? ? nil : ret.join("\n")
end

#serialize_constants(source, rbi, klass, is_singleton, source_symbols, rbi_symbols) ⇒ Object



206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/hidden-definition-finder.rb', line 206

def serialize_constants(source, rbi, klass, is_singleton, source_symbols, rbi_symbols)
  source_by_name = source.map {|v| [v["name"]["name"], v]}.to_h
  ret = []

  rbi.each do |rbi_entry|
    # skip duplicated constant fields
    next if rbi_entry["name"]["kind"] == "UNIQUE" and rbi_entry["name"]["unique"] == "MANGLE_RENAME"

    source_entry = source_by_name[rbi_entry["name"]["name"]]

    ret << serialize_alias(source_entry, rbi_entry, klass, source_symbols, rbi_symbols)
    ret << serialize_class(source_entry, rbi_entry, klass, source_symbols, rbi_symbols, source_by_name)
  end

  ret.compact.join("\n")
end

#symbols_id_to_name(entry, prefix) ⇒ Object



186
187
188
189
190
# File 'lib/hidden-definition-finder.rb', line 186

def symbols_id_to_name(entry, prefix)
  ret = {}
  symbols_id_to_name_real(entry, prefix, ret)
  ret
end

#write_constantsObject



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/hidden-definition-finder.rb', line 106

def write_constants
  puts "Printing your code's symbol table into #{SOURCE_CONSTANTS}"
  io = IO.popen(
    [
      File.realpath("#{__dir__}/../bin/srb"),
      'tc',
      '--print=symbol-table-full-json',
      '--stdout-hup-hack',
      '--silence-dev-message',
      '--no-error-count',
      '-e', # this is additive with any files / dirs
      '""',
    ],
    err: SOURCE_CONSTANTS_ERR
  )
  File.write(SOURCE_CONSTANTS, io.read)
  io.close
  raise "Your source can't be read by Sorbet.\nYou can try `find . -type f | xargs -L 1 -t bundle exec srb tc --no-config --isolate-error-code 1000` and hopefully the last file it is processing before it dies is the culprit.\nIf not, maybe the errors in this file will help: #{SOURCE_CONSTANTS_ERR}" if File.read(SOURCE_CONSTANTS).empty?

  puts "Printing #{TMP_RBI}'s symbol table into #{RBI_CONSTANTS}"
  io = IO.popen(
    [
      {'SRB_SKIP_GEM_RBIS' => 'true'},
      File.realpath("#{__dir__}/../bin/srb"),
      'tc',
      # Make sure we don't load a sorbet/config in your cwd
      '--no-config',
      '--print=symbol-table-full-json',
      # The hidden-definition serializer is not smart enough to put T::Enum
      # constants it discovers inside an `enums do` block. We probably want
      # to come up with a better long term solution here.
      '--suppress-error-code=3506',
      # Method redefined with mismatched argument is ok since sometime
      # people monkeypatch over method
      '--suppress-error-code=4010',
      # Redefining constant is needed because we serialize things both as
      # aliases and in-class constants.
      '--suppress-error-code=4012',
      # At one point we split 4012 into 4012/4022
      '--suppress-error-code=4022',
      # Invalid nesting is ok because we don't generate all the intermediate
      # namespaces for aliases
      '--suppress-error-code=4015',
      # The `Random` class has a super class of `Object` in ruby-2.7 and
      # `Random::Base` in ruby-2. We can remove this once we no longer support
      # ruby-2.7.
      '--suppress-error-code=5012',
      '--stdout-hup-hack',
      '--silence-dev-message',
      '--no-error-count',
      TMP_RBI,
    ],
    err: RBI_CONSTANTS_ERR
  )
  File.write(RBI_CONSTANTS, io.read)
  io.close
  raise "#{TMP_RBI} had unexpected errors. Check this file for a clue: #{RBI_CONSTANTS_ERR}" unless $?.success?
end

#write_diff(source, rbi) ⇒ Object



173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/hidden-definition-finder.rb', line 173

def write_diff(source, rbi)
  puts "Building rbi id to symbol map"
  rbi_symbols = symbols_id_to_name(rbi, '')
  puts "Building source id to symbol map"
  source_symbols = symbols_id_to_name(source, '')
  puts "Writing #{DIFF_RBI}"
  diff = serialize_constants(
    source.fetch("children", []),
    rbi.fetch("children", []),
    Object, false, source_symbols, rbi_symbols)
  File.write(DIFF_RBI, diff)
end