steep 0.43.0 → 0.45.0

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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +8 -0
  3. data/.github/workflows/ruby.yml +3 -2
  4. data/.gitignore +0 -1
  5. data/CHANGELOG.md +30 -0
  6. data/Gemfile +0 -1
  7. data/Gemfile.lock +77 -0
  8. data/bin/output_test.rb +8 -2
  9. data/lib/steep.rb +4 -1
  10. data/lib/steep/ast/builtin.rb +7 -1
  11. data/lib/steep/ast/types/factory.rb +19 -25
  12. data/lib/steep/diagnostic/ruby.rb +137 -60
  13. data/lib/steep/diagnostic/signature.rb +34 -0
  14. data/lib/steep/equatable.rb +21 -0
  15. data/lib/steep/index/source_index.rb +55 -5
  16. data/lib/steep/interface/block.rb +4 -0
  17. data/lib/steep/interface/function.rb +798 -579
  18. data/lib/steep/server/interaction_worker.rb +239 -20
  19. data/lib/steep/server/master.rb +40 -19
  20. data/lib/steep/server/type_check_worker.rb +68 -0
  21. data/lib/steep/services/file_loader.rb +26 -19
  22. data/lib/steep/services/goto_service.rb +322 -0
  23. data/lib/steep/services/hover_content.rb +131 -79
  24. data/lib/steep/services/type_check_service.rb +25 -0
  25. data/lib/steep/source.rb +7 -10
  26. data/lib/steep/type_construction.rb +496 -518
  27. data/lib/steep/type_inference/block_params.rb +2 -5
  28. data/lib/steep/type_inference/method_params.rb +483 -0
  29. data/lib/steep/type_inference/send_args.rb +610 -128
  30. data/lib/steep/typing.rb +46 -21
  31. data/lib/steep/version.rb +1 -1
  32. data/sig/steep/type_inference/send_args.rbs +42 -0
  33. data/smoke/array/test_expectations.yml +3 -3
  34. data/smoke/block/c.rb +0 -1
  35. data/smoke/class/test_expectations.yml +12 -15
  36. data/smoke/const/test_expectations.yml +0 -10
  37. data/smoke/diagnostics-rbs/mixin-class-error.rbs +6 -0
  38. data/smoke/diagnostics-rbs/test_expectations.yml +12 -0
  39. data/smoke/diagnostics-ruby-unsat/Steepfile +5 -0
  40. data/smoke/diagnostics-ruby-unsat/a.rbs +3 -0
  41. data/smoke/diagnostics-ruby-unsat/test_expectations.yml +27 -0
  42. data/smoke/{diagnostics → diagnostics-ruby-unsat}/unsatisfiable_constraint.rb +0 -1
  43. data/smoke/diagnostics/a.rbs +0 -4
  44. data/smoke/diagnostics/different_method_parameter_kind.rb +9 -0
  45. data/smoke/diagnostics/method_arity_mismatch.rb +2 -2
  46. data/smoke/diagnostics/method_parameter_mismatch.rb +10 -0
  47. data/smoke/diagnostics/test_expectations.yml +108 -57
  48. data/smoke/ensure/test_expectations.yml +3 -3
  49. data/smoke/enumerator/test_expectations.yml +1 -1
  50. data/smoke/literal/test_expectations.yml +2 -2
  51. data/smoke/method/test_expectations.yml +11 -10
  52. data/smoke/regression/issue_372.rb +8 -0
  53. data/smoke/regression/issue_372.rbs +4 -0
  54. data/smoke/regression/test_expectations.yml +0 -12
  55. data/smoke/rescue/test_expectations.yml +3 -3
  56. data/smoke/toplevel/test_expectations.yml +3 -3
  57. data/smoke/tsort/test_expectations.yml +2 -2
  58. data/steep.gemspec +2 -2
  59. metadata +24 -10
@@ -7,6 +7,8 @@ module Steep
7
7
  HoverJob = Struct.new(:id, :path, :line, :column, keyword_init: true)
8
8
  CompletionJob = Struct.new(:id, :path, :line, :column, :trigger, keyword_init: true)
9
9
 
10
+ LSP = LanguageServer::Protocol
11
+
10
12
  attr_reader :service
11
13
 
12
14
  def initialize(project:, reader:, writer:, queue: Queue.new)
@@ -65,7 +67,7 @@ module Steep
65
67
  uri = URI.parse(params[:textDocument][:uri])
66
68
  path = project.relative_path(Pathname(uri.path))
67
69
  line, column = params[:position].yield_self {|hash| [hash[:line]+1, hash[:character]] }
68
- trigger = params[:context][:triggerCharacter]
70
+ trigger = params.dig(:context, :triggerCharacter)
69
71
 
70
72
  queue << CompletionJob.new(id: id, path: path, line: line, column: column, trigger: trigger)
71
73
  end
@@ -77,11 +79,12 @@ module Steep
77
79
  Steep.logger.info { "path=#{job.path}, line=#{job.line}, column=#{job.column}" }
78
80
 
79
81
  hover = Services::HoverContent.new(service: service)
80
- content = hover.content_for(path: job.path, line: job.line, column: job.column+1)
82
+ content = hover.content_for(path: job.path, line: job.line, column: job.column)
81
83
  if content
82
84
  range = content.location.yield_self do |location|
83
- start_position = { line: location.line - 1, character: location.column }
84
- end_position = { line: location.last_line - 1, character: location.last_column }
85
+ lsp_range = location.as_lsp_range
86
+ start_position = { line: lsp_range[:start][:line], character: lsp_range[:start][:character] }
87
+ end_position = { line: lsp_range[:end][:line], character: lsp_range[:end][:character] }
85
88
  { start: start_position, end: end_position }
86
89
  end
87
90
 
@@ -99,6 +102,36 @@ module Steep
99
102
 
100
103
  def format_hover(content)
101
104
  case content
105
+ when Services::HoverContent::TypeAliasContent
106
+ comment = content.decl.comment&.string || ''
107
+
108
+ <<-MD
109
+ #{comment}
110
+
111
+ ```rbs
112
+ #{retrieve_decl_information(content.decl)}
113
+ ```
114
+ MD
115
+ when Services::HoverContent::InterfaceContent
116
+ comment = content.decl.comment&.string || ''
117
+
118
+ <<-MD
119
+ #{comment}
120
+
121
+ ```rbs
122
+ #{retrieve_decl_information(content.decl)}
123
+ ```
124
+ MD
125
+ when Services::HoverContent::ClassContent
126
+ comment = content.decl.comment&.string || ''
127
+
128
+ <<-MD
129
+ #{comment}
130
+
131
+ ```rbs
132
+ #{retrieve_decl_information(content.decl)}
133
+ ```
134
+ MD
102
135
  when Services::HoverContent::VariableContent
103
136
  "`#{content.name}`: `#{content.type.to_s}`"
104
137
  when Services::HoverContent::MethodCallContent
@@ -151,32 +184,207 @@ HOVER
151
184
  Steep.logger.tagged("#response_to_completion") do
152
185
  Steep.measure "Generating response" do
153
186
  Steep.logger.info "path: #{job.path}, line: #{job.line}, column: #{job.column}, trigger: #{job.trigger}"
187
+ case
188
+ when target = project.target_for_source_path(job.path)
189
+ file = service.source_files[job.path] or return
190
+ subtyping = service.signature_services[target.name].current_subtyping or return
191
+
192
+ provider = Services::CompletionProvider.new(source_text: file.content, path: job.path, subtyping: subtyping)
193
+ items = begin
194
+ provider.run(line: job.line, column: job.column)
195
+ rescue Parser::SyntaxError
196
+ []
197
+ end
198
+
199
+ completion_items = items.map do |item|
200
+ format_completion_item(item)
201
+ end
202
+
203
+ Steep.logger.debug "items = #{completion_items.inspect}"
204
+
205
+ LSP::Interface::CompletionList.new(
206
+ is_incomplete: false,
207
+ items: completion_items
208
+ )
209
+ when (_, targets = project.targets_for_path(job.path))
210
+ target = targets[0] or return
211
+ sig_service = service.signature_services[target.name]
212
+ relative_path = job.path
213
+ buffer = RBS::Buffer.new(name: relative_path, content: sig_service.files[relative_path].content)
214
+ pos = buffer.loc_to_pos([job.line, job.column])
215
+ prefix = buffer.content[0...pos].reverse[/\A[\w\d]*/].reverse
216
+
217
+ case sig_service.status
218
+ when Steep::Services::SignatureService::SyntaxErrorStatus, Steep::Services::SignatureService::AncestorErrorStatus
219
+ return
220
+ end
221
+
222
+ decls = sig_service.files[relative_path].decls
223
+ locator = RBS::Locator.new(decls: decls)
224
+
225
+ hd, tail = locator.find2(line: job.line, column: job.column)
226
+
227
+ namespace = []
228
+ tail.each do |t|
229
+ case t
230
+ when RBS::AST::Declarations::Module, RBS::AST::Declarations::Class
231
+ namespace << t.name.to_namespace
232
+ end
233
+ end
234
+ context = []
154
235
 
155
- target = project.target_for_source_path(job.path) or return
156
- file = service.source_files[job.path] or return
157
- subtyping = service.signature_services[target.name].current_subtyping or return
236
+ namespace.each do |ns|
237
+ context.map! { |n| ns + n }
238
+ context << ns
239
+ end
240
+
241
+ context.map!(&:absolute!)
242
+
243
+ class_items = sig_service.latest_env.class_decls.keys.map { |type_name|
244
+ format_completion_item_for_rbs(sig_service, type_name, context, job, prefix)
245
+ }.compact
246
+
247
+ alias_items = sig_service.latest_env.alias_decls.keys.map { |type_name|
248
+ format_completion_item_for_rbs(sig_service, type_name, context, job, prefix)
249
+ }.compact
250
+
251
+ interface_items = sig_service.latest_env.interface_decls.keys.map {|type_name|
252
+ format_completion_item_for_rbs(sig_service, type_name, context, job, prefix)
253
+ }.compact
158
254
 
159
- provider = Services::CompletionProvider.new(source_text: file.content, path: job.path, subtyping: subtyping)
160
- items = begin
161
- provider.run(line: job.line, column: job.column)
162
- rescue Parser::SyntaxError
163
- []
164
- end
255
+ completion_items = class_items + alias_items + interface_items
165
256
 
166
- completion_items = items.map do |item|
167
- format_completion_item(item)
257
+ LSP::Interface::CompletionList.new(
258
+ is_incomplete: false,
259
+ items: completion_items
260
+ )
168
261
  end
262
+ end
263
+ end
264
+ end
265
+
266
+ def format_completion_item_for_rbs(sig_service, type_name, context, job, prefix)
267
+ range = LanguageServer::Protocol::Interface::Range.new(
268
+ start: LanguageServer::Protocol::Interface::Position.new(
269
+ line: job.line - 1,
270
+ character: job.column - prefix.size
271
+ ),
272
+ end: LanguageServer::Protocol::Interface::Position.new(
273
+ line: job.line - 1,
274
+ character: job.column - prefix.size
275
+ )
276
+ )
169
277
 
170
- Steep.logger.debug "items = #{completion_items.inspect}"
278
+ name = relative_name_in_context(type_name, context).to_s
171
279
 
172
- LSP::Interface::CompletionList.new(
173
- is_incomplete: false,
174
- items: completion_items
175
- )
280
+ return unless name.start_with?(prefix)
281
+
282
+ case type_name.kind
283
+ when :class
284
+ class_decl = sig_service.latest_env.class_decls[type_name]&.decls[0]&.decl or raise
285
+
286
+ LanguageServer::Protocol::Interface::CompletionItem.new(
287
+ label: "#{name}",
288
+ documentation: format_comment(class_decl.comment),
289
+ text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
290
+ range: range,
291
+ new_text: name
292
+ ),
293
+ kind: LSP::Constant::CompletionItemKind::CLASS,
294
+ insert_text_format: LSP::Constant::InsertTextFormat::SNIPPET
295
+
296
+ )
297
+ when :alias
298
+ alias_decl = sig_service.latest_env.alias_decls[type_name]&.decl or raise
299
+ LanguageServer::Protocol::Interface::CompletionItem.new(
300
+ label: "#{name}",
301
+ text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
302
+ range: range,
303
+ new_text: name
304
+ ),
305
+ documentation: format_comment(alias_decl.comment),
306
+ # https://github.com/microsoft/vscode-languageserver-node/blob/6d78fc4d25719b231aba64a721a606f58b9e0a5f/client/src/common/client.ts#L624-L650
307
+ kind: LSP::Constant::CompletionItemKind::FIELD,
308
+ insert_text_format: LSP::Constant::InsertTextFormat::SNIPPET
309
+ )
310
+ when :interface
311
+ interface_decl = sig_service.latest_env.interface_decls[type_name]&.decl or raise
312
+
313
+ LanguageServer::Protocol::Interface::CompletionItem.new(
314
+ label: "#{name}",
315
+ text_edit: LanguageServer::Protocol::Interface::TextEdit.new(
316
+ range: range,
317
+ new_text: name
318
+ ),
319
+ documentation: format_comment(interface_decl.comment),
320
+ kind: LanguageServer::Protocol::Constant::CompletionItemKind::INTERFACE,
321
+ insert_text_format: LanguageServer::Protocol::Constant::InsertTextFormat::SNIPPET
322
+ )
323
+ end
324
+ end
325
+
326
+ def format_comment(comment)
327
+ if comment
328
+ LSP::Interface::MarkupContent.new(
329
+ kind: LSP::Constant::MarkupKind::MARKDOWN,
330
+ value: comment.string
331
+ )
332
+ end
333
+ end
334
+
335
+ def name_and_params(name, params)
336
+ if params.empty?
337
+ "#{name}"
338
+ else
339
+ ps = params.each.map do |param|
340
+ s = ""
341
+ if param.skip_validation
342
+ s << "unchecked "
343
+ end
344
+ case param.variance
345
+ when :invariant
346
+ # nop
347
+ when :covariant
348
+ s << "out "
349
+ when :contravariant
350
+ s << "in "
351
+ end
352
+ s + param.name.to_s
353
+ end
354
+
355
+ "#{name}[#{ps.join(", ")}]"
356
+ end
357
+ end
358
+
359
+ def name_and_args(name, args)
360
+ if name && args
361
+ if args.empty?
362
+ "#{name}"
363
+ else
364
+ "#{name}[#{args.join(", ")}]"
176
365
  end
177
366
  end
178
367
  end
179
368
 
369
+ def retrieve_decl_information(decl)
370
+ case decl
371
+ when RBS::AST::Declarations::Class
372
+ super_class = if super_class = decl.super_class
373
+ " < #{name_and_args(super_class.name, super_class.args)}"
374
+ end
375
+ "class #{name_and_params(decl.name, decl.type_params)}#{super_class}"
376
+ when RBS::AST::Declarations::Module
377
+ self_type = unless decl.self_types.empty?
378
+ " : #{decl.self_types.join(", ")}"
379
+ end
380
+ "module #{name_and_params(decl.name, decl.type_params)}#{self_type}"
381
+ when RBS::AST::Declarations::Alias
382
+ "type #{decl.name} = #{decl.type}"
383
+ when RBS::AST::Declarations::Interface
384
+ "interface #{name_and_params(decl.name, decl.type_params)}"
385
+ end
386
+ end
387
+
180
388
  def format_completion_item(item)
181
389
  range = LanguageServer::Protocol::Interface::Range.new(
182
390
  start: LanguageServer::Protocol::Interface::Position.new(
@@ -297,6 +505,17 @@ HOVER
297
505
 
298
506
  params.join(", ")
299
507
  end
508
+
509
+ def relative_name_in_context(type_name, context)
510
+ context.each do |namespace|
511
+ if (type_name.to_s == namespace.to_type_name.to_s || type_name.namespace.to_s == "::")
512
+ return RBS::TypeName.new(namespace: RBS::Namespace.empty, name: type_name.name)
513
+ elsif type_name.to_s.start_with?(namespace.to_s)
514
+ return TypeName(type_name.to_s.sub(namespace.to_type_name.to_s, '')).relative!
515
+ end
516
+ end
517
+ type_name
518
+ end
300
519
  end
301
520
  end
302
521
  end
@@ -499,7 +499,11 @@ module Steep
499
499
  trigger_characters: [".", "@"],
500
500
  work_done_progress: true
501
501
  ),
502
- workspace_symbol_provider: true
502
+ workspace_symbol_provider: true,
503
+ definition_provider: true,
504
+ declaration_provider: false,
505
+ implementation_provider: true,
506
+ type_definition_provider: false
503
507
  )
504
508
  )
505
509
  }
@@ -509,8 +513,9 @@ module Steep
509
513
 
510
514
  when "initialized"
511
515
  if typecheck_automatically
512
- request = controller.make_request(include_unchanged: true)
513
- start_type_check(request, last_request: nil, start_progress: request.total > 10)
516
+ if request = controller.make_request(include_unchanged: true)
517
+ start_type_check(request, last_request: nil, start_progress: request.total > 10)
518
+ end
514
519
  end
515
520
 
516
521
  when "textDocument/didChange"
@@ -520,12 +525,13 @@ module Steep
520
525
 
521
526
  when "textDocument/didSave"
522
527
  if typecheck_automatically
523
- request = controller.make_request(last_request: current_type_check_request)
524
- start_type_check(
525
- request,
526
- last_request: current_type_check_request,
527
- start_progress: request.total > 10
528
- )
528
+ if request = controller.make_request(last_request: current_type_check_request)
529
+ start_type_check(
530
+ request,
531
+ last_request: current_type_check_request,
532
+ start_progress: request.total > 10
533
+ )
534
+ end
529
535
  end
530
536
 
531
537
  when "textDocument/didOpen"
@@ -582,17 +588,37 @@ module Steep
582
588
  end
583
589
  end
584
590
 
591
+ when "textDocument/definition", "textDocument/implementation"
592
+ result_controller << group_request do |group|
593
+ typecheck_workers.each do |worker|
594
+ group << send_request(method: message[:method], params: message[:params], worker: worker)
595
+ end
596
+
597
+ group.on_completion do |handlers|
598
+ links = handlers.flat_map(&:result)
599
+ job_queue << SendMessageJob.to_client(
600
+ message: {
601
+ id: message[:id],
602
+ result: links
603
+ }
604
+ )
605
+ end
606
+ end
607
+
585
608
  when "$/typecheck"
586
609
  request = controller.make_request(
587
610
  guid: message[:params][:guid],
588
611
  last_request: current_type_check_request,
589
612
  include_unchanged: true
590
613
  )
591
- start_type_check(
592
- request,
593
- last_request: current_type_check_request,
594
- start_progress: true
595
- )
614
+
615
+ if request
616
+ start_type_check(
617
+ request,
618
+ last_request: current_type_check_request,
619
+ start_progress: true
620
+ )
621
+ end
596
622
 
597
623
  when "shutdown"
598
624
  result_controller << group_request do |group|
@@ -637,11 +663,6 @@ module Steep
637
663
 
638
664
  def start_type_check(request, last_request:, start_progress:)
639
665
  Steep.logger.tagged "#start_type_check(#{request.guid}, #{last_request&.guid}" do
640
- unless request
641
- Steep.logger.info "Skip start type checking"
642
- return
643
- end
644
-
645
666
  if last_request
646
667
  Steep.logger.info "Cancelling last request"
647
668
 
@@ -11,6 +11,31 @@ module Steep
11
11
  TypeCheckCodeJob = Struct.new(:guid, :path, keyword_init: true)
12
12
  ValidateAppSignatureJob = Struct.new(:guid, :path, keyword_init: true)
13
13
  ValidateLibrarySignatureJob = Struct.new(:guid, :path, keyword_init: true)
14
+ GotoJob = Struct.new(:id, :kind, :params, keyword_init: true) do
15
+ def self.implementation(id:, params:)
16
+ new(
17
+ kind: :implementation,
18
+ id: id,
19
+ params: params
20
+ )
21
+ end
22
+
23
+ def self.definition(id:, params:)
24
+ new(
25
+ kind: :definition,
26
+ id: id,
27
+ params: params
28
+ )
29
+ end
30
+
31
+ def implementation?
32
+ kind == :implementation
33
+ end
34
+
35
+ def definition?
36
+ kind == :definition
37
+ end
38
+ end
14
39
 
15
40
  include ChangeBuffer
16
41
 
@@ -44,6 +69,10 @@ module Steep
44
69
  when "$/typecheck/start"
45
70
  params = request[:params]
46
71
  enqueue_typecheck_jobs(params)
72
+ when "textDocument/definition"
73
+ queue << GotoJob.definition(id: request[:id], params: request[:params])
74
+ when "textDocument/implementation"
75
+ queue << GotoJob.implementation(id: request[:id], params: request[:params])
47
76
  end
48
77
  end
49
78
 
@@ -179,6 +208,11 @@ module Steep
179
208
  id: job.id,
180
209
  result: stats_result().map(&:as_json)
181
210
  )
211
+ when GotoJob
212
+ writer.write(
213
+ id: job.id,
214
+ result: goto(job)
215
+ )
182
216
  end
183
217
  end
184
218
 
@@ -231,6 +265,40 @@ module Steep
231
265
  end
232
266
  end
233
267
  end
268
+
269
+ def goto(job)
270
+ path = Pathname(URI.parse(job.params[:textDocument][:uri]).path)
271
+ line = job.params[:position][:line] + 1
272
+ column = job.params[:position][:character]
273
+
274
+ goto_service = Services::GotoService.new(type_check: service)
275
+ locations =
276
+ case
277
+ when job.definition?
278
+ goto_service.definition(path: path, line: line, column: column)
279
+ when job.implementation?
280
+ goto_service.implementation(path: path, line: line, column: column)
281
+ else
282
+ raise
283
+ end
284
+
285
+ locations.map do |loc|
286
+ path =
287
+ case loc
288
+ when RBS::Location
289
+ Pathname(loc.buffer.name)
290
+ else
291
+ Pathname(loc.source_buffer.name)
292
+ end
293
+
294
+ path = project.absolute_path(path)
295
+
296
+ {
297
+ uri: URI.parse(path.to_s).tap {|uri| uri.scheme = "file" }.to_s,
298
+ range: loc.as_lsp_range
299
+ }
300
+ end
301
+ end
234
302
  end
235
303
  end
236
304
  end