steep 1.9.3 → 1.10.0.dev.1

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.
@@ -178,6 +178,7 @@ module Steep
178
178
  attr_reader :job_queue, :write_queue
179
179
 
180
180
  attr_reader :current_type_check_request
181
+ attr_reader :refork_mutex
181
182
  attr_reader :controller
182
183
  attr_reader :result_controller
183
184
 
@@ -185,7 +186,7 @@ module Steep
185
186
  attr_accessor :typecheck_automatically
186
187
  attr_reader :start_type_checking_queue
187
188
 
188
- def initialize(project:, reader:, writer:, interaction_worker:, typecheck_workers:, queue: Queue.new)
189
+ def initialize(project:, reader:, writer:, interaction_worker:, typecheck_workers:, queue: Queue.new, refork: false)
189
190
  @project = project
190
191
  @reader = reader
191
192
  @writer = writer
@@ -196,6 +197,8 @@ module Steep
196
197
  @commandline_args = []
197
198
  @job_queue = queue
198
199
  @write_queue = SizedQueue.new(100)
200
+ @refork_mutex = Mutex.new
201
+ @need_to_refork = refork
199
202
 
200
203
  @controller = TypeCheckController.new(project: project)
201
204
  @result_controller = ResultController.new()
@@ -244,8 +247,10 @@ module Steep
244
247
  Steep.logger.info { "Processing SendMessageJob: dest=client, method=#{job.message[:method] || "-"}, id=#{job.message[:id] || "-"}" }
245
248
  writer.write job.message
246
249
  when WorkerProcess
247
- Steep.logger.info { "Processing SendMessageJob: dest=#{job.dest.name}, method=#{job.message[:method] || "-"}, id=#{job.message[:id] || "-"}" }
248
- job.dest << job.message
250
+ refork_mutex.synchronize do
251
+ Steep.logger.info { "Processing SendMessageJob: dest=#{job.dest.name}, method=#{job.message[:method] || "-"}, id=#{job.message[:id] || "-"}" }
252
+ job.dest << job.message
253
+ end
249
254
  end
250
255
  end
251
256
  end
@@ -287,11 +292,13 @@ module Steep
287
292
  end
288
293
  end
289
294
 
290
- waiter = ThreadWaiter.new
291
- each_worker do |worker|
292
- waiter << worker.wait_thread
295
+ waiter = ThreadWaiter.new(each_worker.to_a) {|worker| worker.wait_thread }
296
+ # @type var th: Thread & WorkerProcess::_ProcessWaitThread
297
+ while th = _ = waiter.wait_one()
298
+ if each_worker.any? { |worker| worker.pid == th.pid }
299
+ break # The worker unexpectedly exited
300
+ end
293
301
  end
294
- waiter.wait_one()
295
302
 
296
303
  unless job_queue.closed?
297
304
  # Exit by error
@@ -413,49 +420,7 @@ module Steep
413
420
  end
414
421
 
415
422
  if file_system_watcher_supported?
416
- patterns = [] #: Array[String]
417
- project.targets.each do |target|
418
- target.source_pattern.patterns.each do |pat|
419
- path = project.base_dir + pat
420
- patterns << path.to_s unless path.directory?
421
- end
422
- target.source_pattern.prefixes.each do |pat|
423
- path = project.base_dir + pat
424
- patterns << (path + "**/*.rb").to_s unless path.file?
425
- end
426
- target.signature_pattern.patterns.each do |pat|
427
- path = project.base_dir + pat
428
- patterns << path.to_s unless path.directory?
429
- end
430
- target.signature_pattern.prefixes.each do |pat|
431
- path = project.base_dir + pat
432
- patterns << (path + "**/*.rbs").to_s unless path.file?
433
- end
434
- end
435
- patterns.sort!
436
- patterns.uniq!
437
-
438
- Steep.logger.info { "Setting up didChangeWatchedFiles with pattern: #{patterns.inspect}" }
439
-
440
- enqueue_write_job SendMessageJob.to_client(
441
- message: {
442
- id: SecureRandom.uuid,
443
- method: "client/registerCapability",
444
- params: {
445
- registrations: [
446
- {
447
- id: SecureRandom.uuid,
448
- method: "workspace/didChangeWatchedFiles",
449
- registerOptions: {
450
- watchers: patterns.map do |pattern|
451
- { globPattern: pattern }
452
- end
453
- }
454
- }
455
- ]
456
- }
457
- }
458
- )
423
+ setup_file_system_watcher()
459
424
  end
460
425
 
461
426
  controller.changed_paths.clear()
@@ -635,7 +600,7 @@ module Steep
635
600
  enqueue_write_job SendMessageJob.to_client(
636
601
  message: {
637
602
  id: message[:id],
638
- result: []
603
+ result: [] #: Array[untyped]
639
604
  }
640
605
  )
641
606
  end
@@ -800,7 +765,7 @@ module Steep
800
765
  Steep.logger.info "Starting new progress..."
801
766
 
802
767
  @current_type_check_request = request
803
-
768
+
804
769
  if progress
805
770
  # If `request:` keyword arg is not given
806
771
  request.work_done_progress.begin("Type checking", request_id: fresh_request_id)
@@ -836,6 +801,76 @@ module Steep
836
801
  if current.finished?
837
802
  finish_type_check(current)
838
803
  @current_type_check_request = nil
804
+ refork_workers
805
+ end
806
+ end
807
+ end
808
+ end
809
+
810
+ def refork_workers
811
+ return unless @need_to_refork
812
+ @need_to_refork = false
813
+
814
+ Thread.new do
815
+ Thread.current.abort_on_exception = true
816
+
817
+ primary, *others = typecheck_workers
818
+ primary or raise
819
+ others.each do |worker|
820
+ worker.index or raise
821
+
822
+ refork_mutex.synchronize do
823
+ refork_finished = Thread::Queue.new
824
+ stdin_in, stdin_out = IO.pipe
825
+ stdout_in, stdout_out = IO.pipe
826
+
827
+ result_controller << send_refork_request(params: { index: worker.index, max_index: typecheck_workers.size }, worker: primary) do |handler|
828
+ handler.on_completion do |response|
829
+ writer = LanguageServer::Protocol::Transport::Io::Writer.new(stdin_out)
830
+ reader = LanguageServer::Protocol::Transport::Io::Reader.new(stdout_in)
831
+
832
+ pid = response[:result][:pid]
833
+ # It does not need to wait worker process
834
+ # because the primary worker monitors it instead.
835
+ #
836
+ # @type var wait_thread: Thread & WorkerProcess::_ProcessWaitThread
837
+ wait_thread = _ = Thread.new { sleep }
838
+ wait_thread.define_singleton_method(:pid) { pid }
839
+
840
+ new_worker = WorkerProcess.new(reader:, writer:, stderr: nil, wait_thread:, name: "#{worker.name}-2", index: worker.index)
841
+ old_worker = typecheck_workers[worker.index] or raise
842
+
843
+ typecheck_workers[(new_worker.index or raise)] = new_worker
844
+
845
+ original_old_worker = old_worker.dup
846
+ old_worker.redirect_to new_worker
847
+
848
+ refork_finished << true
849
+
850
+ result_controller << send_request(method: 'shutdown', worker: original_old_worker) do |handler|
851
+ handler.on_completion do
852
+ send_request(method: 'exit', worker: original_old_worker)
853
+ end
854
+ end
855
+
856
+ Thread.new do
857
+ tags = Steep.logger.formatter.current_tags.dup
858
+ Steep.logger.formatter.push_tags(*tags, "from-worker@#{new_worker.name}")
859
+ new_worker.reader.read do |message|
860
+ job_queue << ReceiveMessageJob.new(source: new_worker, message: message)
861
+ end
862
+ end
863
+ end
864
+ end
865
+
866
+ # The primary worker starts forking when it receives the IOs.
867
+ primary.io_socket or raise
868
+ primary.io_socket.send_io(stdin_in)
869
+ primary.io_socket.send_io(stdout_out)
870
+ stdin_in.close
871
+ stdout_out.close
872
+
873
+ refork_finished.pop
839
874
  end
840
875
  end
841
876
  end
@@ -868,6 +903,25 @@ module Steep
868
903
  end
869
904
  end
870
905
 
906
+ def send_refork_request(id: fresh_request_id(), params:, worker:, &block)
907
+ method = CustomMethods::Refork::METHOD
908
+ Steep.logger.info "Sending request #{method}(#{id}) to #{worker.name}"
909
+
910
+ # @type var message: lsp_request
911
+ message = { method: method, id: id, params: params }
912
+ ResultHandler.new(request: message).tap do |handler|
913
+ yield handler if block
914
+
915
+ job = SendMessageJob.to_worker(worker, message: message)
916
+ case job.dest
917
+ when WorkerProcess
918
+ job.dest << job.message
919
+ else
920
+ raise "Unexpected destination: #{job.dest}"
921
+ end
922
+ end
923
+ end
924
+
871
925
  def group_request()
872
926
  GroupHandler.new().tap do |group|
873
927
  yield group
@@ -907,6 +961,58 @@ module Steep
907
961
  )
908
962
  end
909
963
  end
964
+
965
+ def setup_file_system_watcher()
966
+ patterns = [] #: Array[String]
967
+
968
+ project.targets.each do |target|
969
+ patterns.concat(paths_to_watch(target.source_pattern, extname: ".rb"))
970
+ patterns.concat(paths_to_watch(target.signature_pattern, extname: ".rbs"))
971
+ target.groups.each do |group|
972
+ patterns.concat(paths_to_watch(group.source_pattern, extname: ".rb"))
973
+ patterns.concat(paths_to_watch(group.signature_pattern, extname: ".rbs"))
974
+ end
975
+ end
976
+ patterns.sort!
977
+ patterns.uniq!
978
+
979
+ Steep.logger.info { "Setting up didChangeWatchedFiles with pattern: #{patterns.inspect}" }
980
+
981
+ enqueue_write_job SendMessageJob.to_client(
982
+ message: {
983
+ id: SecureRandom.uuid,
984
+ method: "client/registerCapability",
985
+ params: {
986
+ registrations: [
987
+ {
988
+ id: SecureRandom.uuid,
989
+ method: "workspace/didChangeWatchedFiles",
990
+ registerOptions: {
991
+ watchers: patterns.map do |pattern|
992
+ { globPattern: pattern }
993
+ end
994
+ }
995
+ }
996
+ ]
997
+ }
998
+ }
999
+ )
1000
+ end
1001
+
1002
+ def paths_to_watch(pattern, extname:)
1003
+ result = [] #: Array[String]
1004
+
1005
+ pattern.patterns.each do |pat|
1006
+ path = project.base_dir + pat
1007
+ result << path.to_s unless path.directory?
1008
+ end
1009
+ pattern.prefixes.each do |pat|
1010
+ path = project.base_dir + pat
1011
+ result << (path + "**/*#{extname}").to_s unless path.file?
1012
+ end
1013
+
1014
+ result
1015
+ end
910
1016
  end
911
1017
  end
912
1018
  end
@@ -51,15 +51,28 @@ module Steep
51
51
 
52
52
  include ChangeBuffer
53
53
 
54
- def initialize(project:, reader:, writer:, assignment:, commandline_args:)
54
+ attr_reader :io_socket
55
+
56
+ def initialize(project:, reader:, writer:, assignment:, commandline_args:, io_socket: nil, buffered_changes: nil, service: nil)
55
57
  super(project: project, reader: reader, writer: writer)
56
58
 
57
59
  @assignment = assignment
58
- @buffered_changes = {}
60
+ @buffered_changes = buffered_changes || {}
59
61
  @mutex = Mutex.new()
60
62
  @queue = Queue.new
61
63
  @commandline_args = commandline_args
62
64
  @current_type_check_guid = nil
65
+ @io_socket = io_socket
66
+ @service = service if service
67
+ @child_pids = []
68
+
69
+ if io_socket
70
+ Signal.trap "SIGCHLD" do
71
+ while pid = Process.wait(-1, Process::WNOHANG)
72
+ raise "Unexpected worker process exit: #{pid}" if @child_pids.include?(pid)
73
+ end
74
+ end
75
+ end
63
76
  end
64
77
 
65
78
  def service
@@ -98,6 +111,39 @@ module Steep
98
111
  queue << GotoJob.implementation(id: request[:id], params: request[:params])
99
112
  when "textDocument/typeDefinition"
100
113
  queue << GotoJob.type_definition(id: request[:id], params: request[:params])
114
+ when CustomMethods::Refork::METHOD
115
+ io_socket or raise
116
+
117
+ # Receive IOs before fork to avoid receiving them from multiple processes
118
+ stdin = io_socket.recv_io
119
+ stdout = io_socket.recv_io
120
+
121
+ if pid = fork
122
+ stdin.close
123
+ stdout.close
124
+ @child_pids << pid
125
+ writer.write(CustomMethods::Refork.response(request[:id], { pid: }))
126
+ else
127
+ io_socket.close
128
+
129
+ reader.close
130
+ writer.close
131
+
132
+ reader = LanguageServer::Protocol::Transport::Io::Reader.new(stdin)
133
+ writer = LanguageServer::Protocol::Transport::Io::Writer.new(stdout)
134
+ Steep.logger.info("Reforked worker: #{Process.pid}, params: #{request[:params]}")
135
+ index = request[:params][:index]
136
+ assignment = Services::PathAssignment.new(max_index: request[:params][:max_index], index: index)
137
+
138
+ worker = self.class.new(project: project, reader: reader, writer: writer, assignment: assignment, commandline_args: commandline_args, io_socket: nil, buffered_changes: buffered_changes, service: service)
139
+
140
+ tags = Steep.logger.formatter.current_tags.dup
141
+ tags[tags.find_index("typecheck:typecheck@0")] = "typecheck:typecheck@#{index}-reforked"
142
+ Steep.logger.formatter.push_tags(tags)
143
+ worker.run()
144
+
145
+ raise "unreachable"
146
+ end
101
147
  end
102
148
  end
103
149
 
@@ -8,32 +8,30 @@ module Steep
8
8
  attr_reader :name
9
9
  attr_reader :wait_thread
10
10
  attr_reader :index
11
+ attr_reader :io_socket
11
12
 
12
- def initialize(reader:, writer:, stderr:, wait_thread:, name:, index: nil)
13
+ def initialize(reader:, writer:, io_socket: nil, stderr:, wait_thread:, name:, index: nil)
13
14
  @reader = reader
14
15
  @writer = writer
15
16
  @stderr = stderr
17
+ @io_socket = io_socket
16
18
  @wait_thread = wait_thread
17
19
  @name = name
18
20
  @index = index
19
21
  end
20
22
 
21
23
  def self.start_worker(type, name:, steepfile:, steep_command:, index: nil, delay_shutdown: false, patterns: [])
22
- begin
23
- unless steep_command
24
- fork_worker(
25
- type,
26
- name: name,
27
- steepfile: steepfile,
28
- index: index,
29
- delay_shutdown: delay_shutdown,
30
- patterns: patterns
31
- )
32
- else
33
- # Use `#spawn_worker`
34
- raise NotImplementedError
35
- end
36
- rescue NotImplementedError
24
+ if Steep.can_fork? && !steep_command
25
+ fork_worker(
26
+ type,
27
+ name: name,
28
+ steepfile: steepfile,
29
+ index: index,
30
+ is_primary: index && (index[1] == 0 && index[0] >= 2),
31
+ delay_shutdown: delay_shutdown,
32
+ patterns: patterns
33
+ )
34
+ else
37
35
  spawn_worker(
38
36
  type,
39
37
  name: name,
@@ -46,9 +44,10 @@ module Steep
46
44
  end
47
45
  end
48
46
 
49
- def self.fork_worker(type, name:, steepfile:, index:, delay_shutdown:, patterns:)
47
+ def self.fork_worker(type, name:, steepfile:, index:, delay_shutdown:, patterns:, is_primary:)
50
48
  stdin_in, stdin_out = IO.pipe
51
49
  stdout_in, stdout_out = IO.pipe
50
+ sock_master, sock_worker = UNIXSocket.socketpair if is_primary
52
51
 
53
52
  worker = Drivers::Worker.new(stdout: stdout_out, stdin: stdin_in, stderr: STDERR)
54
53
 
@@ -61,12 +60,14 @@ module Steep
61
60
  worker.index = this
62
61
  end
63
62
  worker.commandline_args = patterns
63
+ worker.io_socket = sock_worker
64
64
 
65
65
  pid = fork do
66
66
  Process.setpgid(0, 0)
67
67
  Steep.ui_logger.level = :fatal
68
68
  stdin_out.close
69
69
  stdout_in.close
70
+ sock_master&.close
70
71
  worker.run()
71
72
  end
72
73
 
@@ -81,6 +82,7 @@ module Steep
81
82
 
82
83
  stdin_in.close
83
84
  stdout_out.close
85
+ sock_worker&.close
84
86
 
85
87
  new(
86
88
  reader: reader,
@@ -88,7 +90,8 @@ module Steep
88
90
  stderr: STDERR,
89
91
  wait_thread: wait_thread,
90
92
  name: name,
91
- index: index&.[](1)
93
+ index: index&.[](1),
94
+ io_socket: sock_master,
92
95
  )
93
96
  end
94
97
 
@@ -146,6 +149,10 @@ module Steep
146
149
  end
147
150
  end
148
151
 
152
+ def redirect_to(worker)
153
+ @writer = worker.writer
154
+ end
155
+
149
156
  def <<(message)
150
157
  writer.write(message)
151
158
  end
@@ -155,11 +162,11 @@ module Steep
155
162
  end
156
163
 
157
164
  def kill(force: false)
158
- Steep.logger.tagged("WorkerProcess#kill@#{name}(#{wait_thread.pid})") do
165
+ Steep.logger.tagged("WorkerProcess#kill@#{name}(#{pid})") do
159
166
  begin
160
167
  signal = force ? :KILL : :TERM
161
168
  Steep.logger.debug("Sending signal SIG#{signal}...")
162
- Process.kill(signal, wait_thread.pid)
169
+ Process.kill(signal, pid)
163
170
  Steep.logger.debug("Successfully sent the signal.")
164
171
  rescue Errno::ESRCH => error
165
172
  Steep.logger.debug("Failed #{error.inspect}")
@@ -171,6 +178,10 @@ module Steep
171
178
  end
172
179
  end
173
180
  end
181
+
182
+ def pid
183
+ wait_thread.pid
184
+ end
174
185
  end
175
186
  end
176
187
  end
@@ -51,9 +51,29 @@ module Steep
51
51
  raise
52
52
  end
53
53
  end
54
+
55
+ def deprecated?
56
+ annotations =
57
+ case entry = env.constant_entry(full_name)
58
+ when RBS::Environment::ConstantEntry
59
+ entry.decl.annotations
60
+ when RBS::Environment::ClassEntry, RBS::Environment::ModuleEntry
61
+ entry.decls.flat_map { _1.decl.annotations }
62
+ when RBS::Environment::ClassAliasEntry, RBS::Environment::ModuleAliasEntry
63
+ entry.decl.annotations
64
+ else
65
+ raise
66
+ end
67
+
68
+ if AnnotationsHelper.deprecated_annotation?(annotations)
69
+ true
70
+ else
71
+ false
72
+ end
73
+ end
54
74
  end
55
75
 
56
- SimpleMethodNameItem = _ = Struct.new(:identifier, :range, :receiver_type, :method_types, :method_member, :method_name, keyword_init: true) do
76
+ SimpleMethodNameItem = _ = Struct.new(:identifier, :range, :receiver_type, :method_types, :method_member, :method_name, :deprecated, keyword_init: true) do
57
77
  # @implements SimpleMethodNameItem
58
78
 
59
79
  def comment
@@ -640,13 +660,15 @@ module Steep
640
660
  all_members.each do |member|
641
661
  associated_decl = all_decls.find {|decl| decl.method_def.member == member } or next
642
662
  overloads = method_entry.overloads.select {|overload| overload.method_defs.any? {|defn| defn.member == member }}
663
+ annotations = associated_decl.method_def.member_annotations
643
664
  items << SimpleMethodNameItem.new(
644
665
  identifier: name,
645
666
  range: range,
646
667
  receiver_type: type,
647
668
  method_name: associated_decl.method_name,
648
669
  method_types: overloads.map {|overload| subtyping.factory.method_type_1(overload.method_type) },
649
- method_member: member
670
+ method_member: member,
671
+ deprecated: AnnotationsHelper.deprecated_annotation?(annotations) ? true : false
650
672
  )
651
673
  end
652
674
  else
@@ -295,7 +295,7 @@ module Steep
295
295
 
296
296
  unless errors.empty?
297
297
  # Builder won't be used.
298
- factory = AST::Types::Factory.new(builder: _ = nil)
298
+ factory = AST::Types::Factory.new(builder: RBS::DefinitionBuilder.new(env: env, ancestor_builder: builder))
299
299
  return errors.map {|error| Diagnostic::Signature.from_rbs_error(error, factory: factory) }
300
300
  end
301
301
 
@@ -302,7 +302,22 @@ module Steep
302
302
  def self.type_check(source:, subtyping:, constant_resolver:, cursor:)
303
303
  annotations = source.annotations(block: source.node, factory: subtyping.factory, context: nil)
304
304
 
305
- definition = subtyping.factory.definition_builder.build_instance(AST::Builtin::Object.module_name)
305
+ case annotations.self_type
306
+ when AST::Types::Name::Instance
307
+ module_name = annotations.self_type.name
308
+ module_type = AST::Types::Name::Singleton.new(name: module_name)
309
+ instance_type = annotations.self_type
310
+ when AST::Types::Name::Singleton
311
+ module_name = annotations.self_type.name
312
+ module_type = annotations.self_type
313
+ instance_type = annotations.self_type
314
+ else
315
+ module_name = AST::Builtin::Object.module_name
316
+ module_type = AST::Builtin::Object.module_type
317
+ instance_type = AST::Builtin::Object.instance_type
318
+ end
319
+
320
+ definition = subtyping.factory.definition_builder.build_instance(module_name)
306
321
 
307
322
  const_env = TypeInference::ConstantEnv.new(
308
323
  factory: subtyping.factory,
@@ -321,17 +336,17 @@ module Steep
321
336
  context = TypeInference::Context.new(
322
337
  block_context: nil,
323
338
  module_context: TypeInference::Context::ModuleContext.new(
324
- instance_type: AST::Builtin::Object.instance_type,
325
- module_type: AST::Builtin::Object.module_type,
339
+ instance_type: instance_type,
340
+ module_type: module_type,
326
341
  implement_name: nil,
327
342
  nesting: nil,
328
- class_name: AST::Builtin::Object.module_name,
329
- instance_definition: subtyping.factory.definition_builder.build_instance(AST::Builtin::Object.module_name),
330
- module_definition: subtyping.factory.definition_builder.build_singleton(AST::Builtin::Object.module_name)
343
+ class_name: module_name,
344
+ instance_definition: subtyping.factory.definition_builder.build_instance(module_name),
345
+ module_definition: subtyping.factory.definition_builder.build_singleton(module_name)
331
346
  ),
332
347
  method_context: nil,
333
348
  break_context: nil,
334
- self_type: AST::Builtin::Object.instance_type,
349
+ self_type: instance_type,
335
350
  type_env: type_env,
336
351
  call_context: TypeInference::MethodCall::TopLevelContext.new,
337
352
  variable_context: TypeInference::Context::TypeVariableContext.empty
data/lib/steep/source.rb CHANGED
@@ -498,6 +498,8 @@ module Steep
498
498
  # Skip
499
499
  when :kwargs
500
500
  # skip
501
+ when :when
502
+ # skip
501
503
  when :pair
502
504
  key, value = node.children
503
505
  key = insert_type_node(key, comments.except(last_line))
@@ -644,7 +646,7 @@ module Steep
644
646
  receiver_node, name, _, location = deconstruct_send_node!(send_node)
645
647
 
646
648
  if receiver_node
647
- if location.dot
649
+ if location.dot && location.selector
648
650
  location.selector.line
649
651
  end
650
652
  else
@@ -285,11 +285,6 @@ module Steep
285
285
  )
286
286
  end
287
287
 
288
- when relation.sub_type.is_a?(AST::Types::Self) && !self_type.is_a?(AST::Types::Self)
289
- Expand(relation) do
290
- check_type(Relation.new(sub_type: self_type, super_type: relation.super_type))
291
- end
292
-
293
288
  when relation.sub_type.is_a?(AST::Types::Instance) && !instance_type.is_a?(AST::Types::Instance)
294
289
  Expand(relation) do
295
290
  check_type(Relation.new(sub_type: instance_type, super_type: relation.super_type))
@@ -419,6 +414,11 @@ module Steep
419
414
  end
420
415
  end
421
416
 
417
+ when relation.sub_type.is_a?(AST::Types::Self) && !self_type.is_a?(AST::Types::Self)
418
+ Expand(relation) do
419
+ check_type(Relation.new(sub_type: self_type, super_type: relation.super_type))
420
+ end
421
+
422
422
  when relation.super_type.is_a?(AST::Types::Name::Interface)
423
423
  Expand(relation) do
424
424
  check_interface(
@@ -493,7 +493,7 @@ module Steep
493
493
  end
494
494
 
495
495
  when relation.sub_type.is_a?(AST::Types::Tuple) && relation.super_type.is_a?(AST::Types::Tuple)
496
- if relation.sub_type.types.size >= relation.super_type.types.size
496
+ if relation.sub_type.types.size == relation.super_type.types.size
497
497
  pairs = relation.sub_type.types.take(relation.super_type.types.size).zip(relation.super_type.types)
498
498
 
499
499
  All(relation) do |result|
@@ -541,21 +541,28 @@ module Steep
541
541
 
542
542
  when relation.sub_type.is_a?(AST::Types::Record) && relation.super_type.is_a?(AST::Types::Record)
543
543
  All(relation) do |result|
544
+ unchecked_keys = Set.new(relation.sub_type.elements.each_key)
545
+
544
546
  relation.super_type.elements.each_key do |key|
545
- super_element_type = relation.super_type.elements[key]
547
+ super_element_type = relation.super_type.elements.fetch(key) #: AST::Types::t
548
+ sub_element_type = relation.sub_type.elements.fetch(key, nil) #: AST::Types::t?
546
549
 
547
- if relation.sub_type.elements.key?(key)
548
- sub_element_type = relation.sub_type.elements[key]
550
+ if relation.super_type.required?(key)
551
+ rel = Relation.new(sub_type: sub_element_type || AST::Builtin.nil_type, super_type: super_element_type)
552
+ result.add(rel) { check_type(rel) }
549
553
  else
550
- if relation.super_type.required?(key)
551
- sub_element_type = AST::Builtin.nil_type
554
+ # If the key is optional, it's okay to not have the key
555
+ if sub_element_type
556
+ rel = Relation.new(sub_type: sub_element_type, super_type: super_element_type)
557
+ result.add(rel) { check_type(rel) }
552
558
  end
553
559
  end
554
560
 
555
- if sub_element_type
556
- rel = Relation.new(sub_type: sub_element_type, super_type: super_element_type)
557
- result.add(rel) { check_type(rel) }
558
- end
561
+ unchecked_keys.delete(key)
562
+ end
563
+
564
+ unless unchecked_keys.empty?
565
+ return Failure(relation, Result::Failure::UnknownPairError.new(relation: relation))
559
566
  end
560
567
  end
561
568
 
@@ -601,7 +608,6 @@ module Steep
601
608
  Expand(relation) do
602
609
  check_type(Relation.new(sub_type: relation.sub_type.back_type, super_type: relation.super_type))
603
610
  end
604
-
605
611
  else
606
612
  Failure(relation, Result::Failure::UnknownPairError.new(relation: relation))
607
613
  end