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')
BLACKLIST =

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



37
38
39
# File 'lib/hidden-definition-finder.rb', line 37

def self.main
  self.new.main
end

.output_fileObject



433
434
435
# File 'lib/hidden-definition-finder.rb', line 433

def self.output_file
  PATH
end

Instance Method Details

#all_modules_and_aliasesObject



73
74
75
76
# File 'lib/hidden-definition-finder.rb', line 73

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

#capture_stderrObject



385
386
387
388
389
390
391
392
# File 'lib/hidden-definition-finder.rb', line 385

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

#constant_cacheObject



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

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

#gen_source_rbi(classes, aliases) ⇒ Object



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/hidden-definition-finder.rb', line 82

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



301
302
303
# File 'lib/hidden-definition-finder.rb', line 301

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

#mainObject



41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/hidden-definition-finder.rb', line 41

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



54
55
56
# File 'lib/hidden-definition-finder.rb', line 54

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

#read_constantsObject



148
149
150
151
152
153
154
# File 'lib/hidden-definition-finder.rb', line 148

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



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

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

#require_everythingObject



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

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

#rm_dirObject



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

def rm_dir
  FileUtils.rm_r(TMP_PATH)
end

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



282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/hidden-definition-finder.rb', line 282

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



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
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
# File 'lib/hidden-definition-finder.rb', line 203

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

  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
  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
  return if real_name(my_klass) == 'T'

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



189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/hidden-definition-finder.rb', line 189

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



169
170
171
172
173
# File 'lib/hidden-definition-finder.rb', line 169

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

#write_constantsObject



102
103
104
105
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
# File 'lib/hidden-definition-finder.rb', line 102

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',
    ],
    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 --error-white-list 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(
    [
      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',
      # Method redefined with mismatched argument is ok since sometime
      # people monkeypatch over method
      '--error-black-list=4010',
      # Redefining constant is needed because we serialize things both as
      # aliases and in-class constants.
      '--error-black-list=4012',
      # Invalid nesting is ok because we don't generate all the intermediate
      # namespaces for aliases
      '--error-black-list=4015',
      '--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



156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/hidden-definition-finder.rb', line 156

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