steep 0.44.0 → 0.47.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 (100) 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 +42 -0
  6. data/Gemfile +0 -3
  7. data/Gemfile.lock +75 -0
  8. data/README.md +2 -1
  9. data/lib/steep/annotation_parser.rb +1 -1
  10. data/lib/steep/ast/builtin.rb +7 -1
  11. data/lib/steep/ast/types/factory.rb +19 -25
  12. data/lib/steep/cli.rb +7 -1
  13. data/lib/steep/diagnostic/lsp_formatter.rb +59 -6
  14. data/lib/steep/diagnostic/ruby.rb +188 -60
  15. data/lib/steep/diagnostic/signature.rb +38 -15
  16. data/lib/steep/drivers/check.rb +3 -0
  17. data/lib/steep/drivers/init.rb +10 -3
  18. data/lib/steep/drivers/utils/driver_helper.rb +15 -0
  19. data/lib/steep/drivers/validate.rb +1 -1
  20. data/lib/steep/drivers/watch.rb +3 -0
  21. data/lib/steep/equatable.rb +21 -0
  22. data/lib/steep/interface/function.rb +798 -579
  23. data/lib/steep/project/dsl.rb +135 -36
  24. data/lib/steep/project/options.rb +13 -53
  25. data/lib/steep/project/target.rb +22 -8
  26. data/lib/steep/server/interaction_worker.rb +245 -26
  27. data/lib/steep/server/master.rb +2 -2
  28. data/lib/steep/server/type_check_worker.rb +6 -9
  29. data/lib/steep/services/file_loader.rb +26 -19
  30. data/lib/steep/services/goto_service.rb +1 -0
  31. data/lib/steep/services/hover_content.rb +135 -80
  32. data/lib/steep/source.rb +12 -11
  33. data/lib/steep/type_construction.rb +435 -502
  34. data/lib/steep/type_inference/block_params.rb +3 -6
  35. data/lib/steep/type_inference/method_params.rb +483 -0
  36. data/lib/steep/type_inference/send_args.rb +599 -128
  37. data/lib/steep/typing.rb +46 -21
  38. data/lib/steep/version.rb +1 -1
  39. data/lib/steep.rb +4 -2
  40. data/sample/Steepfile +10 -3
  41. data/smoke/alias/Steepfile +2 -1
  42. data/smoke/and/Steepfile +2 -1
  43. data/smoke/array/Steepfile +2 -1
  44. data/smoke/array/test_expectations.yml +3 -3
  45. data/smoke/block/Steepfile +2 -2
  46. data/smoke/block/c.rb +0 -1
  47. data/smoke/case/Steepfile +2 -1
  48. data/smoke/class/Steepfile +2 -1
  49. data/smoke/class/test_expectations.yml +12 -15
  50. data/smoke/const/Steepfile +2 -1
  51. data/smoke/diagnostics/Steepfile +2 -1
  52. data/smoke/diagnostics/different_method_parameter_kind.rb +9 -0
  53. data/smoke/diagnostics/method_arity_mismatch.rb +2 -2
  54. data/smoke/diagnostics/method_parameter_mismatch.rb +10 -0
  55. data/smoke/diagnostics/test_expectations.yml +108 -31
  56. data/smoke/diagnostics-rbs/Steepfile +1 -1
  57. data/smoke/diagnostics-rbs/mixin-class-error.rbs +6 -0
  58. data/smoke/diagnostics-rbs/test_expectations.yml +12 -0
  59. data/smoke/diagnostics-rbs-duplicated/Steepfile +2 -1
  60. data/smoke/diagnostics-ruby-unsat/Steepfile +2 -1
  61. data/smoke/dstr/Steepfile +2 -1
  62. data/smoke/ensure/Steepfile +2 -1
  63. data/smoke/ensure/test_expectations.yml +3 -3
  64. data/smoke/enumerator/Steepfile +2 -1
  65. data/smoke/enumerator/test_expectations.yml +1 -1
  66. data/smoke/extension/Steepfile +2 -1
  67. data/smoke/extension/e.rbs +1 -1
  68. data/smoke/hash/Steepfile +2 -1
  69. data/smoke/hello/Steepfile +2 -1
  70. data/smoke/if/Steepfile +2 -1
  71. data/smoke/implements/Steepfile +2 -1
  72. data/smoke/initialize/Steepfile +2 -1
  73. data/smoke/integer/Steepfile +2 -1
  74. data/smoke/interface/Steepfile +2 -1
  75. data/smoke/kwbegin/Steepfile +2 -1
  76. data/smoke/lambda/Steepfile +2 -1
  77. data/smoke/literal/Steepfile +2 -1
  78. data/smoke/literal/test_expectations.yml +2 -2
  79. data/smoke/map/Steepfile +2 -1
  80. data/smoke/method/Steepfile +2 -1
  81. data/smoke/method/test_expectations.yml +11 -10
  82. data/smoke/module/Steepfile +2 -1
  83. data/smoke/regexp/Steepfile +2 -1
  84. data/smoke/regression/Steepfile +2 -1
  85. data/smoke/rescue/Steepfile +2 -1
  86. data/smoke/rescue/test_expectations.yml +3 -3
  87. data/smoke/self/Steepfile +2 -1
  88. data/smoke/skip/Steepfile +2 -1
  89. data/smoke/stdout/Steepfile +2 -1
  90. data/smoke/super/Steepfile +2 -1
  91. data/smoke/toplevel/Steepfile +2 -1
  92. data/smoke/toplevel/test_expectations.yml +3 -3
  93. data/smoke/tsort/Steepfile +4 -5
  94. data/smoke/tsort/test_expectations.yml +2 -2
  95. data/smoke/type_case/Steepfile +2 -1
  96. data/smoke/unexpected/Steepfile +2 -1
  97. data/smoke/yield/Steepfile +2 -1
  98. data/steep.gemspec +2 -2
  99. metadata +16 -10
  100. data/sig/project.rbi +0 -109
@@ -71,46 +71,104 @@ module Steep
71
71
  end
72
72
  end
73
73
 
74
- class IncompatibleArguments < Base
74
+ class UnexpectedPositionalArgument < Base
75
75
  attr_reader :node
76
+ attr_reader :method_type
76
77
  attr_reader :method_name
77
- attr_reader :receiver_type
78
- attr_reader :method_types
79
78
 
80
- def initialize(node:, method_name:, receiver_type:, method_types:)
81
- location = case node.type
82
- when :send
83
- node.loc.selector
84
- when :block
85
- node.children[0].yield_self do |node|
86
- node.loc.selector
87
- end
88
- when :super
89
- node.loc.expression
90
- else
91
- Steep.logger.error { "Unexpected node given: #{node.type} (IncompatibleArguments#initialize)"}
92
- node.loc.expression
93
- end
94
- super(node: node, location: location)
95
- @receiver_type = receiver_type
96
- @method_types = method_types
79
+ def initialize(node:, method_name:, method_type:)
80
+ super(node: node)
97
81
  @method_name = method_name
82
+ @method_type = method_type
98
83
  end
99
84
 
100
85
  def header_line
101
- "Cannot find method `#{method_name}` of type `#{receiver_type}` with compatible arity"
86
+ "Unexpected positional argument"
102
87
  end
88
+ end
103
89
 
104
- def detail_lines
105
- StringIO.new.tap do |io|
106
- io.puts "Method types:"
107
- first_type, *rest_types = method_types
108
- defn = " def #{method_name}"
109
- io.puts "#{defn}: #{first_type}"
110
- rest_types.each do |method_type|
111
- io.puts "#{" " * defn.size}| #{method_type}"
112
- end
113
- end.string.chomp
90
+ class InsufficientPositionalArguments < Base
91
+ attr_reader :node
92
+ attr_reader :method_name
93
+ attr_reader :method_type
94
+
95
+ def initialize(node:, method_name:, method_type:)
96
+ send = case node.type
97
+ when :send, :csend
98
+ node
99
+ when :block, :numblock
100
+ node.children[0]
101
+ end
102
+
103
+ loc = if send
104
+ send.loc.selector.with(end_pos: send.loc.expression.end_pos)
105
+ else
106
+ node.loc.expression
107
+ end
108
+
109
+ super(node: node, location: loc)
110
+ @method_name = method_name
111
+ @method_type = method_type
112
+ end
113
+
114
+ def header_line
115
+ "More positional arguments are required"
116
+ end
117
+ end
118
+
119
+ class UnexpectedKeywordArgument < Base
120
+ attr_reader :node
121
+ attr_reader :method_name
122
+ attr_reader :method_type
123
+
124
+ def initialize(node:, method_name:, method_type:)
125
+ loc = case node.type
126
+ when :pair
127
+ node.children[0].location.expression
128
+ when :kwsplat
129
+ node.location.expression
130
+ else
131
+ raise
132
+ end
133
+ super(node: node, location: loc)
134
+ @method_name = method_name
135
+ @method_type = method_type
136
+ end
137
+
138
+ def header_line
139
+ "Unexpected keyword argument"
140
+ end
141
+ end
142
+
143
+ class InsufficientKeywordArguments < Base
144
+ attr_reader :node
145
+ attr_reader :method_name
146
+ attr_reader :method_type
147
+ attr_reader :missing_keywords
148
+
149
+ def initialize(node:, method_name:, method_type:, missing_keywords:)
150
+ send = case node.type
151
+ when :send, :csend
152
+ node
153
+ when :block, :numblock
154
+ node.children[0]
155
+ end
156
+
157
+ loc = if send
158
+ send.loc.selector.with(end_pos: send.loc.expression.end_pos)
159
+ else
160
+ node.loc.expression
161
+ end
162
+
163
+ super(node: node, location: loc)
164
+
165
+ @method_name = method_name
166
+ @method_type = method_type
167
+ @missing_keywords = missing_keywords
168
+ end
169
+
170
+ def header_line
171
+ "More keyword arguments are required: #{missing_keywords.join(", ")}"
114
172
  end
115
173
  end
116
174
 
@@ -292,6 +350,23 @@ module Steep
292
350
  end
293
351
  end
294
352
 
353
+ class ImplicitBreakValueMismatch < Base
354
+ attr_reader :jump_type
355
+ attr_reader :result
356
+
357
+ include ResultPrinter
358
+
359
+ def initialize(node:, jump_type:, result:)
360
+ super(node: node)
361
+ @jump_type = jump_type
362
+ @result = result
363
+ end
364
+
365
+ def header_line
366
+ "Breaking without a value may result an error because a value of type `#{jump_type}` is expected"
367
+ end
368
+ end
369
+
295
370
  class UnexpectedJump < Base
296
371
  def header_line
297
372
  "Cannot jump from here"
@@ -323,6 +398,36 @@ module Steep
323
398
  end
324
399
  end
325
400
 
401
+ class MethodParameterMismatch < Base
402
+ attr_reader :method_param
403
+ attr_reader :method_type
404
+
405
+ def initialize(method_param:, method_type:)
406
+ super(node: method_param.node)
407
+ @method_param = method_param
408
+ @method_type = method_type
409
+ end
410
+
411
+ def header_line
412
+ "The method parameter is incompatible with the declaration `#{method_type}`"
413
+ end
414
+ end
415
+
416
+ class DifferentMethodParameterKind < Base
417
+ attr_reader :method_param
418
+ attr_reader :method_type
419
+
420
+ def initialize(method_param:, method_type:)
421
+ super(node: method_param.node)
422
+ @method_param = method_param
423
+ @method_type = method_type
424
+ end
425
+
426
+ def header_line
427
+ "The method parameter has different kind from the declaration `#{method_type}`"
428
+ end
429
+ end
430
+
326
431
  class IncompatibleMethodTypeAnnotation < Base
327
432
  attr_reader :interface_method
328
433
  attr_reader :annotation_method
@@ -366,7 +471,7 @@ module Steep
366
471
  include ResultPrinter
367
472
 
368
473
  def initialize(node:, expected:, actual:, result:)
369
- super(node: node)
474
+ super(node: node, location: node.loc.name)
370
475
  @expected = expected
371
476
  @actual = actual
372
477
  @result = result
@@ -552,34 +657,6 @@ module Steep
552
657
  end
553
658
  end
554
659
 
555
- class UnexpectedKeyword < Base
556
- attr_reader :unexpected_keywords
557
-
558
- def initialize(node:, unexpected_keywords:)
559
- super(node: node)
560
- @unexpected_keywords = unexpected_keywords
561
- end
562
-
563
- def header_line
564
- keywords = unexpected_keywords.sort.map {|x| "`#{x}`" }
565
- "Cannot specify unexpected keyword arguments: #{keywords.join(", ")}"
566
- end
567
- end
568
-
569
- class MissingKeyword < Base
570
- attr_reader :missing_keywords
571
-
572
- def initialize(node:, missing_keywords:)
573
- super(node: node)
574
- @missing_keywords = missing_keywords
575
- end
576
-
577
- def header_line
578
- keywords = missing_keywords.sort.map {|x| "`#{x}`" }
579
- "Cannot omit required keywords: #{keywords.join(", ")}"
580
- end
581
- end
582
-
583
660
  class UnsupportedSyntax < Base
584
661
  attr_reader :message
585
662
 
@@ -623,6 +700,57 @@ module Steep
623
700
  "SyntaxError: #{message}"
624
701
  end
625
702
  end
703
+
704
+ ALL = ObjectSpace.each_object(Class).with_object([]) do |klass, array|
705
+ if klass < Base
706
+ array << klass
707
+ end
708
+ end
709
+
710
+ def self.all_error
711
+ @all_error ||= ALL.each.with_object({}) do |klass, hash|
712
+ hash[klass] = LSPFormatter::ERROR
713
+ end.freeze
714
+ end
715
+
716
+ def self.default
717
+ @default ||= all_error.merge(
718
+ {
719
+ ImplicitBreakValueMismatch => :warning,
720
+ FallbackAny => :information,
721
+ ElseOnExhaustiveCase => :warning,
722
+ UnknownConstantAssigned => :warning,
723
+ MethodDefinitionMissing => :information
724
+ }
725
+ ).freeze
726
+ end
727
+
728
+ def self.strict
729
+ @strict ||= all_error.merge(
730
+ {
731
+ NoMethod => nil,
732
+ ImplicitBreakValueMismatch => nil,
733
+ FallbackAny => nil,
734
+ ElseOnExhaustiveCase => nil,
735
+ UnknownConstantAssigned => nil,
736
+ MethodDefinitionMissing => nil
737
+ }
738
+ ).freeze
739
+ end
740
+
741
+ def self.lenient
742
+ @lenient ||= all_error.merge(
743
+ {
744
+ NoMethod => nil,
745
+ ImplicitBreakValueMismatch => nil,
746
+ FallbackAny => nil,
747
+ ElseOnExhaustiveCase => nil,
748
+ UnknownConstantAssigned => nil,
749
+ MethodDefinitionMissing => nil,
750
+ UnexpectedJump => nil
751
+ }
752
+ ).freeze
753
+ end
626
754
  end
627
755
  end
628
756
  end
@@ -38,25 +38,16 @@ module Steep
38
38
  end
39
39
 
40
40
  def self.parser_syntax_error_message(exception)
41
- value = if exception.error_value.is_a?(RBS::Parser::LocatedValue)
42
- exception.error_value.value
43
- else
44
- exception.error_value
45
- end
46
- string = value.to_s
41
+ string = exception.location.source.to_s
47
42
  unless string.empty?
48
43
  string = " (#{string})"
49
44
  end
50
45
 
51
- "Syntax error caused by token `#{exception.token_str}`#{string}"
46
+ "Syntax error caused by token `#{exception.token_type}`#{string}"
52
47
  end
53
48
 
54
49
  def header_line
55
- if exception.is_a?(RBS::Parser::SyntaxError)
56
- SyntaxError.parser_syntax_error_message(exception)
57
- else
58
- exception.message
59
- end
50
+ exception.message
60
51
  end
61
52
  end
62
53
 
@@ -275,6 +266,34 @@ module Steep
275
266
  end
276
267
  end
277
268
 
269
+ class MixinClassError < Base
270
+ attr_reader :member
271
+ attr_reader :type_name
272
+
273
+ def initialize(location:, member:, type_name:)
274
+ super(location: location)
275
+ @member = member
276
+ @type_name = type_name
277
+ end
278
+
279
+ def header_line
280
+ "Cannot #{mixin_name} a class `#{member.name}` in the definition of `#{type_name}`"
281
+ end
282
+
283
+ private
284
+
285
+ def mixin_name
286
+ case member
287
+ when RBS::AST::Members::Prepend
288
+ "prepend"
289
+ when RBS::AST::Members::Include
290
+ "include"
291
+ when RBS::AST::Members::Extend
292
+ "extend"
293
+ end
294
+ end
295
+ end
296
+
278
297
  class UnexpectedError < Base
279
298
  attr_reader :message
280
299
 
@@ -290,10 +309,8 @@ module Steep
290
309
 
291
310
  def self.from_rbs_error(error, factory:)
292
311
  case error
293
- when RBS::Parser::SemanticsError, RBS::Parser::LexerError
312
+ when RBS::ParsingError
294
313
  Diagnostic::Signature::SyntaxError.new(error, location: error.location)
295
- when RBS::Parser::SyntaxError
296
- Diagnostic::Signature::SyntaxError.new(error, location: error.error_value.location)
297
314
  when RBS::DuplicatedDeclarationError
298
315
  Diagnostic::Signature::DuplicatedDeclaration.new(
299
316
  type_name: error.name,
@@ -365,6 +382,12 @@ module Steep
365
382
  param: error.param,
366
383
  location: error.location
367
384
  )
385
+ when RBS::MixinClassError
386
+ Diagnostic::Signature::MixinClassError.new(
387
+ location: error.location,
388
+ type_name: error.type_name,
389
+ member: error.member,
390
+ )
368
391
  else
369
392
  raise error
370
393
  end
@@ -8,6 +8,7 @@ module Steep
8
8
  attr_reader :command_line_patterns
9
9
  attr_accessor :with_expectations_path
10
10
  attr_accessor :save_expectations_path
11
+ attr_accessor :severity_level
11
12
 
12
13
  include Utils::DriverHelper
13
14
  include Utils::JobsCount
@@ -16,6 +17,7 @@ module Steep
16
17
  @stdout = stdout
17
18
  @stderr = stderr
18
19
  @command_line_patterns = []
20
+ @severity_level = :warning
19
21
  end
20
22
 
21
23
  def run
@@ -70,6 +72,7 @@ module Steep
70
72
  case
71
73
  when response[:method] == "textDocument/publishDiagnostics"
72
74
  ds = response[:params][:diagnostics]
75
+ ds.select! {|d| keep_diagnostic?(d) }
73
76
  if ds.empty?
74
77
  stdout.print "."
75
78
  else
@@ -8,6 +8,8 @@ module Steep
8
8
  include Utils::DriverHelper
9
9
 
10
10
  TEMPLATE = <<~EOF
11
+ # D = Steep::Diagnostic
12
+ #
11
13
  # target :lib do
12
14
  # signature "sig"
13
15
  #
@@ -18,15 +20,20 @@ module Steep
18
20
  #
19
21
  # # library "pathname", "set" # Standard libraries
20
22
  # # library "strong_json" # Gems
23
+ #
24
+ # # configure_code_diagnostics(D::Ruby.strict) # `strict` diagnostics setting
25
+ # # configure_code_diagnostics(D::Ruby.lenient) # `lenient` diagnostics setting
26
+ # # configure_code_diagnostics do |hash| # You can setup everything yourself
27
+ # # hash[D::Ruby::NoMethod] = :information
28
+ # # end
21
29
  # end
22
30
 
23
- # target :spec do
31
+ # target :test do
24
32
  # signature "sig", "sig-private"
25
33
  #
26
- # check "spec"
34
+ # check "test"
27
35
  #
28
36
  # # library "pathname", "set" # Standard libraries
29
- # # library "rspec"
30
37
  # end
31
38
  EOF
32
39
 
@@ -55,6 +55,21 @@ module Steep
55
55
  end
56
56
  end
57
57
  end
58
+
59
+ def keep_diagnostic?(diagnostic)
60
+ severity = diagnostic[:severity]
61
+
62
+ case self.severity_level
63
+ when nil, :hint
64
+ true
65
+ when :error
66
+ severity <= LanguageServer::Protocol::Constant::DiagnosticSeverity::ERROR
67
+ when :warning
68
+ severity <= LanguageServer::Protocol::Constant::DiagnosticSeverity::WARNING
69
+ when :information
70
+ severity <= LanguageServer::Protocol::Constant::DiagnosticSeverity::INFORMATION
71
+ end
72
+ end
58
73
  end
59
74
  end
60
75
  end
@@ -36,7 +36,7 @@ module Steep
36
36
 
37
37
  any_error ||= !errors.empty?
38
38
 
39
- formatter = Diagnostic::LSPFormatter.new
39
+ formatter = Diagnostic::LSPFormatter.new({})
40
40
  diagnostics = errors.group_by {|e| e.location.buffer }.transform_values do |errors|
41
41
  errors.map {|error| formatter.format(error) }
42
42
  end
@@ -5,6 +5,7 @@ module Steep
5
5
  attr_reader :stdout
6
6
  attr_reader :stderr
7
7
  attr_reader :queue
8
+ attr_accessor :severity_level
8
9
 
9
10
  include Utils::DriverHelper
10
11
  include Utils::JobsCount
@@ -16,6 +17,7 @@ module Steep
16
17
  @stdout = stdout
17
18
  @stderr = stderr
18
19
  @queue = Thread::Queue.new
20
+ @severity_level = :warning
19
21
  end
20
22
 
21
23
  def watching?(changed_path, files:, dirs:)
@@ -126,6 +128,7 @@ module Steep
126
128
  printer = DiagnosticPrinter.new(stdout: stdout, buffer: buffer)
127
129
 
128
130
  diagnostics = response[:params][:diagnostics]
131
+ diagnostics.filter! {|d| keep_diagnostic?(d) }
129
132
 
130
133
  unless diagnostics.empty?
131
134
  diagnostics.each do |diagnostic|
@@ -0,0 +1,21 @@
1
+ module Steep
2
+ module Equatable
3
+ def ==(other)
4
+ if other.class == self.class
5
+ instance_variables.all? do |name|
6
+ other.instance_variable_get(name) == instance_variable_get(name)
7
+ end
8
+ end
9
+ end
10
+
11
+ def eql?(other)
12
+ self == other
13
+ end
14
+
15
+ def hash
16
+ instance_variables.inject(self.class.hash) do |hash, name|
17
+ hash ^ instance_variable_get(name).hash
18
+ end
19
+ end
20
+ end
21
+ end