sorbet 0.5.5841

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,37 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative './step_interface'
5
+
6
+ require 'fileutils'
7
+
8
+ class Sorbet; end
9
+ module Sorbet::Private; end
10
+ class Sorbet::Private::CreateConfig
11
+ SORBET_DIR = 'sorbet'
12
+ SORBET_CONFIG_FILE = "#{SORBET_DIR}/config"
13
+
14
+ include Sorbet::Private::StepInterface
15
+
16
+ def self.main
17
+ FileUtils.mkdir_p(SORBET_DIR)
18
+
19
+ if File.file?(SORBET_CONFIG_FILE)
20
+ puts "Reusing existing config file: #{SORBET_CONFIG_FILE}"
21
+ return
22
+ end
23
+
24
+ File.open(SORBET_CONFIG_FILE, 'w') do |f|
25
+ f.puts('--dir')
26
+ f.puts('.')
27
+ end
28
+ end
29
+
30
+ def self.output_file
31
+ SORBET_CONFIG_FILE
32
+ end
33
+ end
34
+
35
+ if $PROGRAM_NAME == __FILE__
36
+ Sorbet::Private::CreateConfig.main
37
+ end
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env ruby
2
+ # typed: false
3
+
4
+ require_relative './step_interface'
5
+ require_relative './t'
6
+
7
+ require 'bundler'
8
+ require 'fileutils'
9
+ require 'set'
10
+
11
+ class Sorbet; end
12
+ module Sorbet::Private; end
13
+ class Sorbet::Private::FetchRBIs
14
+ SORBET_DIR = 'sorbet'
15
+ SORBET_CONFIG_FILE = "#{SORBET_DIR}/config"
16
+ SORBET_RBI_LIST = "#{SORBET_DIR}/rbi_list"
17
+ SORBET_RBI_SORBET_TYPED = "#{SORBET_DIR}/rbi/sorbet-typed/"
18
+
19
+ XDG_CACHE_HOME = ENV['XDG_CACHE_HOME'] || "#{ENV['HOME']}/.cache"
20
+ RBI_CACHE_DIR = "#{XDG_CACHE_HOME}/sorbet/sorbet-typed"
21
+
22
+ SORBET_TYPED_REPO = 'https://github.com/sorbet/sorbet-typed.git'
23
+ SORBET_TYPED_REVISION = ENV['SRB_SORBET_TYPED_REVISION'] || 'origin/master'
24
+
25
+ HEADER = Sorbet::Private::Serialize.header(false, 'sorbet-typed')
26
+
27
+ include Sorbet::Private::StepInterface
28
+
29
+ # Ensure our cache is up-to-date
30
+ T::Sig::WithoutRuntime.sig {void}
31
+ def self.fetch_sorbet_typed
32
+ if !File.directory?(RBI_CACHE_DIR)
33
+ IO.popen(["git", "clone", SORBET_TYPED_REPO, RBI_CACHE_DIR]) {|pipe| pipe.read}
34
+ raise "Failed to git pull" if $?.exitstatus != 0
35
+ end
36
+
37
+ FileUtils.cd(RBI_CACHE_DIR) do
38
+ IO.popen(%w{git fetch --all}) {|pipe| pipe.read}
39
+ raise "Failed to git fetch" if $?.exitstatus != 0
40
+ IO.popen(%w{git checkout -q} + [SORBET_TYPED_REVISION]) {|pipe| pipe.read}
41
+ raise "Failed to git checkout" if $?.exitstatus != 0
42
+ end
43
+ end
44
+
45
+ # List of directories whose names satisfy the given Gem::Version (+ 'all/')
46
+ T::Sig::WithoutRuntime.sig do
47
+ params(
48
+ root: String,
49
+ version: Gem::Version,
50
+ )
51
+ .returns(T::Array[String])
52
+ end
53
+ def self.matching_version_directories(root, version)
54
+ paths = Dir.glob("#{root}/*/").select do |dir|
55
+ basename = File.basename(dir.chomp('/'))
56
+ requirements = basename.split(/[,&-]/) # split using ',', '-', or '&'
57
+ requirements.all? do |requirement|
58
+ Gem::Requirement::PATTERN =~ requirement &&
59
+ Gem::Requirement.create(requirement).satisfied_by?(version)
60
+ end
61
+ end
62
+ paths = paths.map {|dir| dir.chomp('/')}
63
+ all_dir = "#{root}/all"
64
+ paths << all_dir if Dir.exist?(all_dir)
65
+ paths
66
+ end
67
+
68
+ # List of directories in lib/ruby whose names satisfy the current RUBY_VERSION
69
+ T::Sig::WithoutRuntime.sig {params(ruby_version: Gem::Version).returns(T::Array[String])}
70
+ def self.paths_for_ruby_version(ruby_version)
71
+ ruby_dir = "#{RBI_CACHE_DIR}/lib/ruby"
72
+ matching_version_directories(ruby_dir, ruby_version)
73
+ end
74
+
75
+ # List of directories in lib/gemspec.name whose names satisfy gemspec.version
76
+ T::Sig::WithoutRuntime.sig {params(gemspec: T.untyped).returns(T::Array[String])}
77
+ def self.paths_for_gem_version(gemspec)
78
+ local_dir = "#{RBI_CACHE_DIR}/lib/#{gemspec.name}"
79
+ matching_version_directories(local_dir, gemspec.version)
80
+ end
81
+
82
+ # Copy the relevant RBIs into their repo, with matching folder structure.
83
+ T::Sig::WithoutRuntime.sig {params(vendor_paths: T::Array[String]).void}
84
+ def self.vendor_rbis_within_paths(vendor_paths)
85
+ vendor_paths.each do |vendor_path|
86
+ relative_vendor_path = vendor_path.sub(RBI_CACHE_DIR, '')
87
+
88
+ dest = "#{SORBET_RBI_SORBET_TYPED}/#{relative_vendor_path}"
89
+ FileUtils.mkdir_p(dest)
90
+
91
+ Dir.glob("#{vendor_path}/*.rbi").each do |rbi|
92
+ extra_header = "#
93
+ # If you would like to make changes to this file, great! Please upstream any changes you make here:
94
+ #
95
+ # https://github.com/sorbet/sorbet-typed/edit/master#{relative_vendor_path}/#{File.basename(rbi)}
96
+ #
97
+ "
98
+ File.write("#{dest}/#{File.basename(rbi)}", HEADER + extra_header + File.read(rbi))
99
+ end
100
+ end
101
+ end
102
+
103
+ T::Sig::WithoutRuntime.sig {void}
104
+ def self.main
105
+ fetch_sorbet_typed
106
+
107
+ gemspecs = Bundler.load.specs.sort_by(&:name)
108
+
109
+ vendor_paths = T.let([], T::Array[String])
110
+ vendor_paths += paths_for_ruby_version(Gem::Version.create(RUBY_VERSION))
111
+ gemspecs.each do |gemspec|
112
+ vendor_paths += paths_for_gem_version(gemspec)
113
+ end
114
+
115
+ # Remove the sorbet-typed directory before repopulating it.
116
+ FileUtils.rm_r(SORBET_RBI_SORBET_TYPED) if Dir.exist?(SORBET_RBI_SORBET_TYPED)
117
+ if vendor_paths.length > 0
118
+ vendor_rbis_within_paths(vendor_paths)
119
+ end
120
+ end
121
+
122
+ def self.output_file
123
+ SORBET_RBI_SORBET_TYPED
124
+ end
125
+ end
126
+
127
+ if $PROGRAM_NAME == __FILE__
128
+ Sorbet::Private::FetchRBIs.main
129
+ end
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+ # typed: false
3
+
4
+ require_relative './step_interface'
5
+ require_relative './t'
6
+
7
+ require 'bundler'
8
+ require 'fileutils'
9
+ require 'set'
10
+ require 'digest'
11
+
12
+ class Sorbet; end
13
+ module Sorbet::Private; end
14
+ class Sorbet::Private::FindGemRBIs
15
+ XDG_CACHE_HOME = ENV['XDG_CACHE_HOME'] || "#{ENV['HOME']}/.cache"
16
+ RBI_CACHE_DIR = "#{XDG_CACHE_HOME}/sorbet/gem-rbis/"
17
+ GEM_DIR = 'rbi'
18
+
19
+ HEADER = Sorbet::Private::Serialize.header(false, 'find-gem-rbis')
20
+
21
+ include Sorbet::Private::StepInterface
22
+
23
+ # List of rbi folders in the gem's source
24
+ T::Sig::WithoutRuntime.sig {params(gemspec: T.untyped).returns(T.nilable(String))}
25
+ def self.paths_within_gem_sources(gemspec)
26
+ gem_rbi = "#{gemspec.full_gem_path}/#{GEM_DIR}"
27
+ gem_rbi if Dir.exist?(gem_rbi)
28
+ end
29
+
30
+ T::Sig::WithoutRuntime.sig {void}
31
+ def self.main
32
+ FileUtils.mkdir_p(RBI_CACHE_DIR) unless Dir.exist?(RBI_CACHE_DIR)
33
+ output_file = File.exist?('Gemfile.lock') ? RBI_CACHE_DIR + Digest::MD5.hexdigest(File.read('Gemfile.lock')) : nil
34
+ return unless output_file
35
+ gemspecs = Bundler.load.specs.sort_by(&:name)
36
+
37
+ gem_source_paths = T.let([], T::Array[String])
38
+ gemspecs.each do |gemspec|
39
+ gem_source_paths << paths_within_gem_sources(gemspec)
40
+ end
41
+
42
+ File.write(output_file, gem_source_paths.compact.join("\n"))
43
+ end
44
+
45
+ def self.output_file
46
+ nil
47
+ end
48
+ end
49
+
50
+ if $PROGRAM_NAME == __FILE__
51
+ Sorbet::Private::FindGemRBIs.main
52
+ end
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # frozen_string_literal: true
4
+ # typed: true
5
+
6
+ class Sorbet; end
7
+ module Sorbet::Private; end
8
+
9
+ require_relative './t'
10
+ require_relative './step_interface'
11
+ require_relative './require_everything'
12
+ require_relative './gem-generator-tracepoint/tracer'
13
+ require_relative './gem-generator-tracepoint/tracepoint_serializer'
14
+
15
+ require 'set'
16
+
17
+ # TODO switch the Struct handling to:
18
+ #
19
+ # class Subclass < Struct(:key1, :key2)
20
+ # end
21
+ #
22
+ # generating:
23
+ #
24
+ # TemporaryStruct = Struct(:key1, :key2)
25
+ # class Subclass < TemporaryStruct
26
+ # end
27
+ #
28
+ # instead of manually defining every getter/setter
29
+
30
+ module Sorbet::Private
31
+ module GemGeneratorTracepoint
32
+ OUTPUT = 'sorbet/rbi/gems/'
33
+
34
+ include Sorbet::Private::StepInterface
35
+
36
+ T::Sig::WithoutRuntime.sig {params(output_dir: String).void}
37
+ def self.main(output_dir = OUTPUT)
38
+ trace_results = Tracer.trace do
39
+ Sorbet::Private::RequireEverything.require_everything
40
+ end
41
+
42
+ FileUtils.rm_r(output_dir) if Dir.exist?(output_dir)
43
+ TracepointSerializer.new(trace_results).serialize(output_dir)
44
+ end
45
+
46
+ def self.output_file
47
+ OUTPUT
48
+ end
49
+ end
50
+ end
51
+
52
+ if $PROGRAM_NAME == __FILE__
53
+ Sorbet::Private::GemGeneratorTracepoint.main
54
+ end
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ require_relative '../serialize'
5
+ require_relative '../real_stdlib'
6
+
7
+ require 'set'
8
+ require 'fileutils'
9
+ require 'delegate'
10
+
11
+
12
+ module Sorbet::Private
13
+ module GemGeneratorTracepoint
14
+ ClassDefinition = Struct.new(:id, :klass, :defs)
15
+
16
+ class TracepointSerializer
17
+ SPECIAL_METHOD_NAMES = %w[! ~ +@ ** -@ * / % + - << >> & | ^ < <= => > >= == === != =~ !~ <=> [] []= `]
18
+
19
+ BAD_METHODS = [
20
+ # These methods don't match the signatures of their parents, so if we let
21
+ # them monkeypatch, they won't be subtypes anymore. Just don't support the
22
+ # bad monkeypatches.
23
+ ['activesupport', 'Time', :to_s],
24
+ ['activesupport', 'Time', :initialize],
25
+
26
+ # These methods cause TracepointSerializer to hang the Ruby process when
27
+ # running Ruby 2.3. See https://github.com/sorbet/sorbet/issues/1145
28
+ ['activesupport', 'ActiveSupport::Deprecation', :new],
29
+ ['activesupport', 'ActiveSupport::Deprecation', :allocate],
30
+ ]
31
+
32
+ HEADER = Sorbet::Private::Serialize.header('true', 'gems')
33
+
34
+ T::Sig::WithoutRuntime.sig {params(files: T::Hash, delegate_classes: T::Hash).void}
35
+ def initialize(files:, delegate_classes:)
36
+ @files = files
37
+ @delegate_classes = delegate_classes
38
+
39
+ @anonymous_map = {}
40
+ @prev_anonymous_id = 0
41
+ end
42
+
43
+ T::Sig::WithoutRuntime.sig {params(output_dir: String).void}
44
+ def serialize(output_dir)
45
+ gem_class_defs = preprocess(@files)
46
+
47
+ FileUtils.mkdir_p(output_dir) unless gem_class_defs.empty?
48
+
49
+ gem_class_defs.each do |gem, klass_ids|
50
+ File.open("#{File.join(output_dir, gem[:gem])}.rbi", 'w') do |f|
51
+ f.write(HEADER)
52
+ f.write("#
53
+ # If you would like to make changes to this file, great! Please create the gem's shim here:
54
+ #
55
+ # https://github.com/sorbet/sorbet-typed/new/master?filename=lib/#{gem[:gem]}/all/#{gem[:gem]}.rbi
56
+ #
57
+ ")
58
+ f.write("# #{gem[:gem]}-#{gem[:version]}\n\n")
59
+ klass_ids.each do |klass_id, class_def|
60
+ klass = class_def.klass
61
+
62
+ f.write("#{Sorbet::Private::RealStdlib.real_is_a?(klass, Class) ? 'class' : 'module'} #{class_name(klass)}")
63
+ f.write(" < #{class_name(klass.superclass)}") if Sorbet::Private::RealStdlib.real_is_a?(klass, Class) && ![Object, nil].include?(klass.superclass)
64
+ f.write("\n")
65
+
66
+ rows = class_def.defs.map do |item|
67
+ case item[:type]
68
+ when :method
69
+ if !valid_method_name?(item[:method])
70
+ # warn("Invalid method name: #{klass}.#{item[:method]}")
71
+ next
72
+ end
73
+ if BAD_METHODS.include?([gem[:gem], class_name(klass), item[:method]])
74
+ next
75
+ end
76
+ begin
77
+ method = item[:singleton] ? Sorbet::Private::RealStdlib.real_method(klass, item[:method]) : klass.instance_method(item[:method])
78
+
79
+ "#{generate_method(method, !item[:singleton])}"
80
+ rescue NameError
81
+ end
82
+ when :include, :extend
83
+ name = class_name(item[item[:type]])
84
+ " #{item[:type]} #{name}"
85
+ end
86
+ end
87
+ rows = rows.compact.sort
88
+ f.write(rows.join("\n"))
89
+ f.write("\n") if !rows.empty?
90
+ f.write("end\n")
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def preprocess(files)
99
+ gem_class_defs = files_to_gem_class_defs(files)
100
+ filter_unused(gem_class_defs)
101
+ end
102
+
103
+ def files_to_gem_class_defs(files)
104
+ # Transform tracer output into hash of gems to class definitions
105
+ files.each_with_object({}) do |(path, defined), gem_class_defs|
106
+ gem = gem_from_location(path)
107
+ if gem.nil?
108
+ warn("Can't find gem for #{path}") unless path.start_with?(Dir.pwd)
109
+ next
110
+ end
111
+ next if gem[:gem] == 'ruby'
112
+ # We're currently ignoring bundler, because we can't easily pin
113
+ # everyone to the same version of bundler in tests and in CI.
114
+ # There is an RBI for bundler in sorbet-typed.
115
+ next if gem[:gem] == 'bundler'
116
+ # We ignore sorbet-runtime because because we write the RBI for it into our payload.
117
+ # For some reason, runtime reflection generates methods with incorrect arities.
118
+ next if gem[:gem] == 'sorbet-runtime'
119
+
120
+ gem_class_defs[gem] ||= {}
121
+ defined.each do |item|
122
+ klass = item[:module]
123
+ klass_id = Sorbet::Private::RealStdlib.real_object_id(klass)
124
+ class_def = gem_class_defs[gem][klass_id] ||= ClassDefinition.new(klass_id, klass, [])
125
+ class_def.defs << item unless item[:type] == :module
126
+ end
127
+ end
128
+ end
129
+
130
+ def filter_unused(gem_class_defs)
131
+ used = detect_used(gem_class_defs)
132
+
133
+ gem_class_defs.each_with_object({}) do |(gem, klass_defs), hsh|
134
+ hsh[gem] = klass_defs.select do |klass_id, klass_def|
135
+ klass = klass_def.klass
136
+
137
+ # Unused anon classes
138
+ next if !((Sorbet::Private::RealStdlib.real_is_a?(klass, Module) && !Sorbet::Private::RealStdlib.real_name(klass).nil?) || used?(klass_id, used))
139
+
140
+ # Anon delegate classes
141
+ next if Sorbet::Private::RealStdlib.real_is_a?(klass, Class) && klass.superclass == Delegator && !klass.name
142
+
143
+ # TODO should this be here?
144
+ # next if [Object, BasicObject, Hash].include?(klass)
145
+ true
146
+ end
147
+ end
148
+ end
149
+
150
+ def detect_used(gem_class_defs)
151
+ # subclassed, included, or extended
152
+ used = {}
153
+
154
+ gem_class_defs.each do |gem, klass_ids|
155
+ klass_ids.each do |klass_id, class_def|
156
+ klass = class_def.klass
157
+
158
+ # only add an anon module if it's used as a superclass of a non-anon module, or is included/extended by a non-anon module
159
+ used_value = Sorbet::Private::RealStdlib.real_is_a?(klass, Module) && !Sorbet::Private::RealStdlib.real_name(klass).nil? ? true : Sorbet::Private::RealStdlib.real_object_id(klass) # if non-anon, set it to true
160
+ (used[Sorbet::Private::RealStdlib.real_object_id(klass.superclass)] ||= Set.new) << used_value if Sorbet::Private::RealStdlib.real_is_a?(klass, Class)
161
+ # otherwise link to next anon class
162
+ class_def.defs.each do |item|
163
+ (used[item[item[:type]].object_id] ||= Set.new) << used_value if [:extend, :include].include?(item[:type])
164
+ end
165
+ end
166
+ end
167
+
168
+ used
169
+ end
170
+
171
+ def used?(klass, used)
172
+ used_by = used[klass] || []
173
+ used_by.any? { |user| user == true || used?(user, used) }
174
+ end
175
+
176
+ def generate_method(method, instance, spaces = 2)
177
+ # method.parameters is an array of:
178
+ # a [:req, :a]
179
+ # b = 1 [:opt, :b]
180
+ # c: [:keyreq, :c]
181
+ # d: 1 [:key, :d]
182
+ # *e [:rest, :e]
183
+ # **f [:keyrest, :f]
184
+ # &g [:block, :g]
185
+ prefix = ' ' * spaces
186
+ parameters = method.parameters.map.with_index do |(type, name), index|
187
+ name = "arg#{index}" if name.nil? || name.empty?
188
+ case type
189
+ when :req
190
+ name
191
+ when :opt
192
+ "#{name} = nil"
193
+ when :keyreq
194
+ "#{name}:"
195
+ when :key
196
+ "#{name}: nil"
197
+ when :rest
198
+ "*#{name}"
199
+ when :keyrest
200
+ "**#{name}"
201
+ when :block
202
+ "&#{name}"
203
+ else
204
+ raise "Unknown parameter type: #{type}"
205
+ end
206
+ end
207
+ parameters = parameters.join(', ')
208
+ parameters = "(#{parameters})" unless parameters.empty?
209
+ "#{prefix}def #{instance ? '' : 'self.'}#{method.name}#{parameters}; end"
210
+ end
211
+
212
+ def anonymous_id
213
+ @prev_anonymous_id += 1
214
+ end
215
+
216
+ def gem_from_location(location)
217
+ match =
218
+ location&.match(/^.*\/(ruby)\/([\d.]+)\//) || # ruby stdlib
219
+ location&.match(/^.*\/(jruby)-([\d.]+)\//) || # jvm ruby stdlib
220
+ location&.match(/^.*\/(site_ruby)\/([\d.]+)\//) || # rubygems
221
+ location&.match(/^.*\/gems\/(?:(?:j?ruby-)?[\d.]+(?:@[^\/]+)?(?:\/bundler)?\/)?gems\/([^\/]+)-([^-\/]+)\//i) # gem
222
+ if match.nil?
223
+ # uncomment to generate files for methods outside of gems
224
+ # {
225
+ # path: location,
226
+ # gem: location.gsub(/[\/\.]/, '_'),
227
+ # version: '1.0.0',
228
+ # }
229
+ nil
230
+ else
231
+ {
232
+ path: match[0],
233
+ gem: match[1],
234
+ version: match[2],
235
+ }
236
+ end
237
+ end
238
+
239
+ def class_name(klass)
240
+ klass = @delegate_classes[Sorbet::Private::RealStdlib.real_object_id(klass)] || klass
241
+ name = Sorbet::Private::RealStdlib.real_name(klass) if Sorbet::Private::RealStdlib.real_is_a?(klass, Module)
242
+
243
+ # class/module has no name; it must be anonymous
244
+ if name.nil? || name == ""
245
+ middle = Sorbet::Private::RealStdlib.real_is_a?(klass, Class) ? klass.superclass : klass.class
246
+ id = @anonymous_map[Sorbet::Private::RealStdlib.real_object_id(klass)] ||= anonymous_id
247
+ return "Anonymous_#{class_name(middle).gsub('::', '_')}_#{id}"
248
+ end
249
+
250
+ # if the name doesn't only contain word characters and ':', or any part doesn't start with a capital, Sorbet doesn't support it
251
+ if name !~ /^[\w:]+$/ || !name.split('::').all? { |part| part =~ /^[A-Z]/ }
252
+ # warn("Invalid class name: #{name}")
253
+ id = @anonymous_map[Sorbet::Private::RealStdlib.real_object_id(klass)] ||= anonymous_id
254
+ return "InvalidName_#{name.gsub(/[^\w]/, '_').gsub(/0x([0-9a-f]+)/, '0x00')}_#{id}"
255
+ end
256
+
257
+ name
258
+ end
259
+
260
+ def valid_method_name?(symbol)
261
+ string = symbol.to_s
262
+ return true if SPECIAL_METHOD_NAMES.include?(string)
263
+ string =~ /^[[:word:]]+[?!=]?$/
264
+ end
265
+ end
266
+ end
267
+ end