sorbet 0.4.4250 → 0.4.4253

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,32 @@
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
+ File.open(SORBET_CONFIG_FILE, 'w') do |f|
20
+ f.puts('--dir')
21
+ f.puts('.')
22
+ end
23
+ end
24
+
25
+ def self.output_file
26
+ SORBET_CONFIG_FILE
27
+ end
28
+ end
29
+
30
+ if $PROGRAM_NAME == __FILE__
31
+ Sorbet::Private::CreateConfig.main
32
+ end
data/lib/fetch-rbis.rb ADDED
@@ -0,0 +1,127 @@
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 origin}) {|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
+ if vendor_paths.length > 0
116
+ vendor_rbis_within_paths(vendor_paths)
117
+ end
118
+ end
119
+
120
+ def self.output_file
121
+ SORBET_RBI_SORBET_TYPED
122
+ end
123
+ end
124
+
125
+ if $PROGRAM_NAME == __FILE__
126
+ Sorbet::Private::FetchRBIs.main
127
+ 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,256 @@
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
+
10
+
11
+ module Sorbet::Private
12
+ module GemGeneratorTracepoint
13
+ ClassDefinition = Struct.new(:id, :klass, :defs)
14
+
15
+ class TracepointSerializer
16
+ SPECIAL_METHOD_NAMES = %w[! ~ +@ ** -@ * / % + - << >> & | ^ < <= => > >= == === != =~ !~ <=> [] []= `]
17
+
18
+ # These methods don't match the signatures of their parents, so if we let
19
+ # them monkeypatch, they won't be subtypes anymore. Just don't support the
20
+ # bad monkeypatches.
21
+ BAD_METHODS = [
22
+ ['activesupport', 'Time', :to_s],
23
+ ['activesupport', 'Time', :initialize],
24
+ ]
25
+
26
+ HEADER = Sorbet::Private::Serialize.header('true', 'gems')
27
+
28
+ T::Sig::WithoutRuntime.sig {params(files: T::Hash, delegate_classes: T::Hash).void}
29
+ def initialize(files:, delegate_classes:)
30
+ @files = files
31
+ @delegate_classes = delegate_classes
32
+
33
+ @anonymous_map = {}
34
+ @prev_anonymous_id = 0
35
+ end
36
+
37
+ T::Sig::WithoutRuntime.sig {params(output_dir: String).void}
38
+ def serialize(output_dir)
39
+ gem_class_defs = preprocess(@files)
40
+
41
+ FileUtils.mkdir_p(output_dir) unless gem_class_defs.empty?
42
+
43
+ gem_class_defs.each do |gem, klass_ids|
44
+ File.open("#{File.join(output_dir, gem[:gem])}.rbi", 'w') do |f|
45
+ f.write(HEADER)
46
+ f.write("#
47
+ # If you would like to make changes to this file, great! Please create the gem's shim here:
48
+ #
49
+ # https://github.com/sorbet/sorbet-typed/new/master?filename=lib/#{gem[:gem]}/all/#{gem[:gem]}.rbi
50
+ #
51
+ ")
52
+ f.write("# #{gem[:gem]}-#{gem[:version]}\n")
53
+ klass_ids.each do |klass_id, class_def|
54
+ klass = class_def.klass
55
+
56
+ f.write("#{Sorbet::Private::RealStdlib.real_is_a?(klass, Class) ? 'class' : 'module'} #{class_name(klass)}")
57
+ f.write(" < #{class_name(klass.superclass)}") if Sorbet::Private::RealStdlib.real_is_a?(klass, Class) && ![Object, nil].include?(klass.superclass)
58
+ f.write("\n")
59
+
60
+ rows = class_def.defs.map do |item|
61
+ case item[:type]
62
+ when :method
63
+ if !valid_method_name?(item[:method])
64
+ # warn("Invalid method name: #{klass}.#{item[:method]}")
65
+ next
66
+ end
67
+ if BAD_METHODS.include?([gem[:gem], class_name(klass), item[:method]])
68
+ next
69
+ end
70
+ begin
71
+ method = item[:singleton] ? klass.method(item[:method]) : klass.instance_method(item[:method])
72
+ "#{generate_method(method, !item[:singleton])}"
73
+ rescue NameError
74
+ end
75
+ when :include, :extend
76
+ name = class_name(item[item[:type]])
77
+ " #{item[:type]} #{name}"
78
+ end
79
+ end
80
+ rows = rows.compact.sort
81
+ f.write(rows.join("\n"))
82
+ f.write("\n") if !rows.empty?
83
+ f.write("end\n")
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def preprocess(files)
92
+ gem_class_defs = files_to_gem_class_defs(files)
93
+ filter_unused(gem_class_defs)
94
+ end
95
+
96
+ def files_to_gem_class_defs(files)
97
+ # Transform tracer output into hash of gems to class definitions
98
+ files.each_with_object({}) do |(path, defined), gem_class_defs|
99
+ gem = gem_from_location(path)
100
+ if gem.nil?
101
+ warn("Can't find gem for #{path}") unless path.start_with?(Dir.pwd)
102
+ next
103
+ end
104
+ next if gem[:gem] == 'ruby'
105
+ # We're currently ignoring bundler, because we can't easily pin
106
+ # everyone to the same version of bundler in tests and in CI.
107
+ # There is an RBI for bundler in sorbet-typed.
108
+ next if gem[:gem] == 'bundler'
109
+
110
+ gem_class_defs[gem] ||= {}
111
+ defined.each do |item|
112
+ klass = item[:module]
113
+ klass_id = Sorbet::Private::RealStdlib.real_object_id(klass)
114
+ class_def = gem_class_defs[gem][klass_id] ||= ClassDefinition.new(klass_id, klass, [])
115
+ class_def.defs << item unless item[:type] == :module
116
+ end
117
+ end
118
+ end
119
+
120
+ def filter_unused(gem_class_defs)
121
+ used = detect_used(gem_class_defs)
122
+
123
+ gem_class_defs.each_with_object({}) do |(gem, klass_defs), hsh|
124
+ hsh[gem] = klass_defs.select do |klass_id, klass_def|
125
+ klass = klass_def.klass
126
+
127
+ # Unused anon classes
128
+ next if !((Sorbet::Private::RealStdlib.real_is_a?(klass, Module) && !Sorbet::Private::RealStdlib.real_name(klass).nil?) || used?(klass_id, used))
129
+
130
+ # Anon delegate classes
131
+ next if Sorbet::Private::RealStdlib.real_is_a?(klass, Class) && klass.superclass == Delegator && !klass.name
132
+
133
+ # TODO should this be here?
134
+ # next if [Object, BasicObject, Hash].include?(klass)
135
+ true
136
+ end
137
+ end
138
+ end
139
+
140
+ def detect_used(gem_class_defs)
141
+ # subclassed, included, or extended
142
+ used = {}
143
+
144
+ gem_class_defs.each do |gem, klass_ids|
145
+ klass_ids.each do |klass_id, class_def|
146
+ klass = class_def.klass
147
+
148
+ # 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
149
+ 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
150
+ (used[Sorbet::Private::RealStdlib.real_object_id(klass.superclass)] ||= Set.new) << used_value if Sorbet::Private::RealStdlib.real_is_a?(klass, Class)
151
+ # otherwise link to next anon class
152
+ class_def.defs.each do |item|
153
+ (used[item[item[:type]].object_id] ||= Set.new) << used_value if [:extend, :include].include?(item[:type])
154
+ end
155
+ end
156
+ end
157
+
158
+ used
159
+ end
160
+
161
+ def used?(klass, used)
162
+ used_by = used[klass] || []
163
+ used_by.any? { |user| user == true || used?(user, used) }
164
+ end
165
+
166
+ def generate_method(method, instance, spaces = 2)
167
+ # method.parameters is an array of:
168
+ # a [:req, :a]
169
+ # b = 1 [:opt, :b]
170
+ # c: [:keyreq, :c]
171
+ # d: 1 [:key, :d]
172
+ # *e [:rest, :e]
173
+ # **f [:keyrest, :f]
174
+ # &g [:block, :g]
175
+ prefix = ' ' * spaces
176
+ parameters = method.parameters.map.with_index do |(type, name), index|
177
+ name = "arg#{index}" if name.nil? || name.empty?
178
+ case type
179
+ when :req
180
+ name
181
+ when :opt
182
+ "#{name} = nil"
183
+ when :keyreq
184
+ "#{name}:"
185
+ when :key
186
+ "#{name}: nil"
187
+ when :rest
188
+ "*#{name}"
189
+ when :keyrest
190
+ "**#{name}"
191
+ when :block
192
+ "&#{name}"
193
+ else
194
+ raise "Unknown parameter type: #{type}"
195
+ end
196
+ end
197
+ parameters = parameters.join(', ')
198
+ parameters = "(#{parameters})" unless parameters.empty?
199
+ "#{prefix}def #{instance ? '' : 'self.'}#{method.name}#{parameters}; end"
200
+ end
201
+
202
+ def anonymous_id
203
+ @prev_anonymous_id += 1
204
+ end
205
+
206
+ def gem_from_location(location)
207
+ match =
208
+ location&.match(/^.*\/(ruby)\/([\d.]+)\//) || # ruby stdlib
209
+ location&.match(/^.*\/(site_ruby)\/([\d.]+)\//) || # rubygems
210
+ location&.match(/^.*\/gems\/(?:ruby-)?[\d.]+(?:\/bundler)?\/gems\/([^\/]+)-([^-\/]+)\//i) # gem
211
+ if match.nil?
212
+ # uncomment to generate files for methods outside of gems
213
+ # {
214
+ # path: location,
215
+ # gem: location.gsub(/[\/\.]/, '_'),
216
+ # version: '1.0.0',
217
+ # }
218
+ nil
219
+ else
220
+ {
221
+ path: match[0],
222
+ gem: match[1],
223
+ version: match[2],
224
+ }
225
+ end
226
+ end
227
+
228
+ def class_name(klass)
229
+ klass = @delegate_classes[Sorbet::Private::RealStdlib.real_object_id(klass)] || klass
230
+ name = Sorbet::Private::RealStdlib.real_name(klass) if Sorbet::Private::RealStdlib.real_is_a?(klass, Module)
231
+
232
+ # class/module has no name; it must be anonymous
233
+ if name.nil? || name == ""
234
+ middle = Sorbet::Private::RealStdlib.real_is_a?(klass, Class) ? klass.superclass : klass.class
235
+ id = @anonymous_map[Sorbet::Private::RealStdlib.real_object_id(klass)] ||= anonymous_id
236
+ return "Anonymous_#{class_name(middle).gsub('::', '_')}_#{id}"
237
+ end
238
+
239
+ # if the name doesn't only contain word characters and ':', or any part doesn't start with a capital, Sorbet doesn't support it
240
+ if name !~ /^[\w:]+$/ || !name.split('::').all? { |part| part =~ /^[A-Z]/ }
241
+ # warn("Invalid class name: #{name}")
242
+ id = @anonymous_map[Sorbet::Private::RealStdlib.real_object_id(klass)] ||= anonymous_id
243
+ return "InvalidName_#{name.gsub(/[^\w]/, '_').gsub(/0x([0-9a-f]+)/, '0x00')}_#{id}"
244
+ end
245
+
246
+ name
247
+ end
248
+
249
+ def valid_method_name?(symbol)
250
+ string = symbol.to_s
251
+ return true if SPECIAL_METHOD_NAMES.include?(string)
252
+ string =~ /^[[:word:]]+[?!=]?$/
253
+ end
254
+ end
255
+ end
256
+ end