steep 0.40.0 → 0.44.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/Gemfile +1 -0
  4. data/bin/output_rebaseline.rb +15 -30
  5. data/bin/output_test.rb +23 -57
  6. data/lib/steep.rb +89 -15
  7. data/lib/steep/annotation_parser.rb +10 -2
  8. data/lib/steep/ast/types/class.rb +4 -0
  9. data/lib/steep/cli.rb +31 -6
  10. data/lib/steep/diagnostic/ruby.rb +13 -8
  11. data/lib/steep/diagnostic/signature.rb +152 -2
  12. data/lib/steep/drivers/annotations.rb +18 -36
  13. data/lib/steep/drivers/check.rb +140 -31
  14. data/lib/steep/drivers/diagnostic_printer.rb +20 -11
  15. data/lib/steep/drivers/langserver.rb +4 -8
  16. data/lib/steep/drivers/print_project.rb +10 -9
  17. data/lib/steep/drivers/stats.rb +135 -119
  18. data/lib/steep/drivers/utils/driver_helper.rb +35 -0
  19. data/lib/steep/drivers/utils/jobs_count.rb +9 -0
  20. data/lib/steep/drivers/validate.rb +29 -18
  21. data/lib/steep/drivers/watch.rb +55 -49
  22. data/lib/steep/drivers/worker.rb +11 -8
  23. data/lib/steep/expectations.rb +159 -0
  24. data/lib/steep/index/signature_symbol_provider.rb +23 -1
  25. data/lib/steep/index/source_index.rb +55 -5
  26. data/lib/steep/interface/block.rb +4 -0
  27. data/lib/steep/project.rb +0 -30
  28. data/lib/steep/project/dsl.rb +5 -3
  29. data/lib/steep/project/pattern.rb +56 -0
  30. data/lib/steep/project/target.rb +11 -227
  31. data/lib/steep/server/base_worker.rb +1 -3
  32. data/lib/steep/server/change_buffer.rb +63 -0
  33. data/lib/steep/server/interaction_worker.rb +72 -57
  34. data/lib/steep/server/master.rb +652 -234
  35. data/lib/steep/server/type_check_worker.rb +304 -0
  36. data/lib/steep/server/worker_process.rb +16 -11
  37. data/lib/steep/{project → services}/completion_provider.rb +5 -5
  38. data/lib/steep/services/content_change.rb +61 -0
  39. data/lib/steep/services/file_loader.rb +48 -0
  40. data/lib/steep/services/goto_service.rb +321 -0
  41. data/lib/steep/{project → services}/hover_content.rb +19 -20
  42. data/lib/steep/services/path_assignment.rb +27 -0
  43. data/lib/steep/services/signature_service.rb +403 -0
  44. data/lib/steep/services/stats_calculator.rb +69 -0
  45. data/lib/steep/services/type_check_service.rb +413 -0
  46. data/lib/steep/signature/validator.rb +187 -85
  47. data/lib/steep/source.rb +21 -18
  48. data/lib/steep/subtyping/check.rb +246 -45
  49. data/lib/steep/subtyping/constraints.rb +4 -4
  50. data/lib/steep/type_construction.rb +428 -193
  51. data/lib/steep/type_inference/block_params.rb +1 -1
  52. data/lib/steep/type_inference/context.rb +22 -0
  53. data/lib/steep/type_inference/local_variable_type_env.rb +26 -12
  54. data/lib/steep/type_inference/logic.rb +1 -1
  55. data/lib/steep/type_inference/logic_type_interpreter.rb +4 -4
  56. data/lib/steep/type_inference/type_env.rb +43 -17
  57. data/lib/steep/version.rb +1 -1
  58. data/smoke/alias/test_expectations.yml +96 -0
  59. data/smoke/and/test_expectations.yml +31 -0
  60. data/smoke/array/test_expectations.yml +103 -0
  61. data/smoke/block/test_expectations.yml +125 -0
  62. data/smoke/case/test_expectations.yml +47 -0
  63. data/smoke/class/test_expectations.yml +120 -0
  64. data/smoke/const/test_expectations.yml +129 -0
  65. data/smoke/diagnostics-rbs-duplicated/test_expectations.yml +13 -0
  66. data/smoke/diagnostics-rbs/Steepfile +7 -4
  67. data/smoke/diagnostics-rbs/test_expectations.yml +231 -0
  68. data/smoke/diagnostics-rbs/unknown-type-name-2.rbs +5 -0
  69. data/smoke/{broken → diagnostics-ruby-unsat}/Steepfile +0 -0
  70. data/smoke/diagnostics-ruby-unsat/a.rbs +3 -0
  71. data/smoke/diagnostics-ruby-unsat/test_expectations.yml +27 -0
  72. data/smoke/{diagnostics → diagnostics-ruby-unsat}/unsatisfiable_constraint.rb +0 -1
  73. data/smoke/diagnostics/a.rbs +0 -4
  74. data/smoke/diagnostics/test_expectations.yml +451 -0
  75. data/smoke/dstr/test_expectations.yml +13 -0
  76. data/smoke/ensure/test_expectations.yml +62 -0
  77. data/smoke/enumerator/test_expectations.yml +135 -0
  78. data/smoke/extension/f.rb +2 -0
  79. data/smoke/extension/f.rbs +3 -0
  80. data/smoke/extension/test_expectations.yml +73 -0
  81. data/smoke/hash/test_expectations.yml +81 -0
  82. data/smoke/hello/test_expectations.yml +25 -0
  83. data/smoke/if/test_expectations.yml +34 -0
  84. data/smoke/implements/b.rb +13 -0
  85. data/smoke/implements/b.rbs +12 -0
  86. data/smoke/implements/test_expectations.yml +23 -0
  87. data/smoke/initialize/test_expectations.yml +1 -0
  88. data/smoke/integer/test_expectations.yml +101 -0
  89. data/smoke/interface/test_expectations.yml +23 -0
  90. data/smoke/kwbegin/test_expectations.yml +17 -0
  91. data/smoke/lambda/test_expectations.yml +39 -0
  92. data/smoke/literal/test_expectations.yml +106 -0
  93. data/smoke/map/test_expectations.yml +1 -0
  94. data/smoke/method/test_expectations.yml +90 -0
  95. data/smoke/module/test_expectations.yml +75 -0
  96. data/smoke/regexp/test_expectations.yml +615 -0
  97. data/smoke/regression/issue_328.rb +1 -0
  98. data/smoke/regression/issue_328.rbs +0 -0
  99. data/smoke/regression/issue_332.rb +11 -0
  100. data/smoke/regression/issue_332.rbs +19 -0
  101. data/smoke/regression/issue_372.rb +8 -0
  102. data/smoke/regression/issue_372.rbs +4 -0
  103. data/smoke/regression/masgn.rb +4 -0
  104. data/smoke/regression/test_expectations.yml +60 -0
  105. data/smoke/regression/thread.rb +7 -0
  106. data/smoke/rescue/test_expectations.yml +79 -0
  107. data/smoke/self/test_expectations.yml +23 -0
  108. data/smoke/skip/test_expectations.yml +23 -0
  109. data/smoke/stdout/test_expectations.yml +1 -0
  110. data/smoke/super/test_expectations.yml +69 -0
  111. data/smoke/toplevel/test_expectations.yml +15 -0
  112. data/smoke/tsort/Steepfile +2 -0
  113. data/smoke/tsort/test_expectations.yml +63 -0
  114. data/smoke/type_case/test_expectations.yml +48 -0
  115. data/smoke/unexpected/Steepfile +5 -0
  116. data/smoke/unexpected/test_expectations.yml +25 -0
  117. data/smoke/unexpected/unexpected.rb +1 -0
  118. data/smoke/unexpected/unexpected.rbs +3 -0
  119. data/smoke/yield/test_expectations.yml +68 -0
  120. data/steep.gemspec +4 -3
  121. metadata +127 -80
  122. data/lib/steep/project/file_loader.rb +0 -68
  123. data/lib/steep/project/signature_file.rb +0 -39
  124. data/lib/steep/project/source_file.rb +0 -129
  125. data/lib/steep/project/stats_calculator.rb +0 -80
  126. data/lib/steep/server/code_worker.rb +0 -150
  127. data/lib/steep/server/signature_worker.rb +0 -157
  128. data/lib/steep/server/utils.rb +0 -69
  129. data/smoke/alias/test.yaml +0 -73
  130. data/smoke/and/test.yaml +0 -24
  131. data/smoke/array/test.yaml +0 -80
  132. data/smoke/block/test.yaml +0 -96
  133. data/smoke/broken/broken.rb +0 -0
  134. data/smoke/broken/broken.rbs +0 -0
  135. data/smoke/broken/test.yaml +0 -6
  136. data/smoke/case/test.yaml +0 -36
  137. data/smoke/class/test.yaml +0 -89
  138. data/smoke/const/test.yaml +0 -96
  139. data/smoke/diagnostics-rbs-duplicated/test.yaml +0 -10
  140. data/smoke/diagnostics-rbs/test.yaml +0 -142
  141. data/smoke/diagnostics/test.yaml +0 -333
  142. data/smoke/dstr/test.yaml +0 -10
  143. data/smoke/ensure/test.yaml +0 -47
  144. data/smoke/enumerator/test.yaml +0 -100
  145. data/smoke/extension/test.yaml +0 -50
  146. data/smoke/hash/test.yaml +0 -62
  147. data/smoke/hello/test.yaml +0 -18
  148. data/smoke/if/test.yaml +0 -27
  149. data/smoke/implements/test.yaml +0 -16
  150. data/smoke/initialize/test.yaml +0 -4
  151. data/smoke/integer/test.yaml +0 -66
  152. data/smoke/interface/test.yaml +0 -16
  153. data/smoke/kwbegin/test.yaml +0 -14
  154. data/smoke/lambda/test.yaml +0 -28
  155. data/smoke/literal/test.yaml +0 -79
  156. data/smoke/map/test.yaml +0 -4
  157. data/smoke/method/test.yaml +0 -71
  158. data/smoke/module/test.yaml +0 -51
  159. data/smoke/regexp/test.yaml +0 -372
  160. data/smoke/regression/test.yaml +0 -38
  161. data/smoke/rescue/test.yaml +0 -60
  162. data/smoke/self/test.yaml +0 -16
  163. data/smoke/skip/test.yaml +0 -16
  164. data/smoke/stdout/test.yaml +0 -4
  165. data/smoke/super/test.yaml +0 -52
  166. data/smoke/toplevel/test.yaml +0 -12
  167. data/smoke/tsort/test.yaml +0 -32
  168. data/smoke/type_case/test.yaml +0 -33
  169. data/smoke/yield/test.yaml +0 -49
@@ -20,6 +20,41 @@ module Steep
20
20
  end
21
21
  end
22
22
  end
23
+
24
+ def request_id
25
+ SecureRandom.alphanumeric(10)
26
+ end
27
+
28
+ def wait_for_response_id(reader:, id:, unknown_responses: :ignore)
29
+ wait_for_message(reader: reader, unknown_messages: unknown_responses) do |response|
30
+ response[:id] == id
31
+ end
32
+ end
33
+
34
+ def shutdown_exit(writer:, reader:)
35
+ request_id().tap do |id|
36
+ writer.write({ method: :shutdown, id: id })
37
+ wait_for_response_id(reader: reader, id: id)
38
+ end
39
+ writer.write({ method: :exit })
40
+ end
41
+
42
+ def wait_for_message(reader:, unknown_messages: :ignore, &block)
43
+ reader.read do |message|
44
+ if yield(message)
45
+ return message
46
+ else
47
+ case unknown_messages
48
+ when :ignore
49
+ # nop
50
+ when :log
51
+ Steep.logger.error { "Unexpected message: #{message.inspect}" }
52
+ when :raise
53
+ raise "Unexpected message: #{message.inspect}"
54
+ end
55
+ end
56
+ end
57
+ end
23
58
  end
24
59
  end
25
60
  end
@@ -0,0 +1,9 @@
1
+ module Steep
2
+ module Drivers
3
+ module Utils
4
+ module JobsCount
5
+ attr_accessor :jobs_count
6
+ end
7
+ end
8
+ end
9
+ end
@@ -13,33 +13,44 @@ module Steep
13
13
 
14
14
  def run
15
15
  project = load_config()
16
+ file_loader = Services::FileLoader.new(base_dir: project.base_dir)
16
17
 
17
- loader = Project::FileLoader.new(project: project)
18
- loader.load_signatures()
19
-
20
- type_check(project)
18
+ any_error = false
21
19
 
22
20
  project.targets.each do |target|
23
- Steep.logger.tagged "target=#{target.name}" do
24
- case (status = target.status)
25
- when Project::Target::SignatureErrorStatus
26
- formatter = Diagnostic::LSPFormatter.new
27
- diagnostics = status.errors.group_by {|e| e.location.buffer }.transform_values do |errors|
28
- errors.map {|error| formatter.format(error) }
21
+ controller = Services::SignatureService.load_from(target.new_env_loader(project: project))
22
+
23
+ changes = file_loader.load_changes(target.signature_pattern, changes: {})
24
+ controller.update(changes)
25
+
26
+ errors =
27
+ Steep.measure "Validation" do
28
+ case controller.status
29
+ when Services::SignatureService::SyntaxErrorStatus, Services::SignatureService::AncestorErrorStatus
30
+ controller.status.diagnostics
31
+ when Services::SignatureService::LoadedStatus
32
+ check = Subtyping::Check.new(factory: AST::Types::Factory.new(builder: controller.latest_builder))
33
+ Signature::Validator.new(checker: check).tap {|v| v.validate() }.each_error.to_a
29
34
  end
35
+ end
30
36
 
31
- diagnostics.each do |buffer, ds|
32
- printer = DiagnosticPrinter.new(stdout: stdout, buffer: buffer)
33
- ds.each do |d|
34
- printer.print(d)
35
- stdout.puts
36
- end
37
- end
37
+ any_error ||= !errors.empty?
38
+
39
+ formatter = Diagnostic::LSPFormatter.new
40
+ diagnostics = errors.group_by {|e| e.location.buffer }.transform_values do |errors|
41
+ errors.map {|error| formatter.format(error) }
42
+ end
43
+
44
+ diagnostics.each do |buffer, ds|
45
+ printer = DiagnosticPrinter.new(stdout: stdout, buffer: buffer)
46
+ ds.each do |d|
47
+ printer.print(d)
48
+ stdout.puts
38
49
  end
39
50
  end
40
51
  end
41
52
 
42
- project.targets.all? {|target| target.status.is_a?(Project::Target::TypeCheckStatus) } ? 0 : 1
53
+ any_error ? 1 : 0
43
54
  end
44
55
  end
45
56
  end
@@ -7,6 +7,7 @@ module Steep
7
7
  attr_reader :queue
8
8
 
9
9
  include Utils::DriverHelper
10
+ include Utils::JobsCount
10
11
 
11
12
  LSP = LanguageServer::Protocol
12
13
 
@@ -17,6 +18,10 @@ module Steep
17
18
  @queue = Thread::Queue.new
18
19
  end
19
20
 
21
+ def watching?(changed_path, files:, dirs:)
22
+ files.empty? || files.include?(changed_path) || dirs.intersect?(changed_path.ascend.to_set)
23
+ end
24
+
20
25
  def run()
21
26
  if dirs.empty?
22
27
  stdout.puts "Specify directories to watch"
@@ -25,10 +30,6 @@ module Steep
25
30
 
26
31
  project = load_config()
27
32
 
28
- loader = Project::FileLoader.new(project: project)
29
- loader.load_sources(dirs)
30
- loader.load_signatures()
31
-
32
33
  client_read, server_write = IO.pipe
33
34
  server_read, client_write = IO.pipe
34
35
 
@@ -38,28 +39,44 @@ module Steep
38
39
  server_reader = LanguageServer::Protocol::Transport::Io::Reader.new(server_read)
39
40
  server_writer = LanguageServer::Protocol::Transport::Io::Writer.new(server_write)
40
41
 
41
- interaction_worker = Server::WorkerProcess.spawn_worker(:interaction, name: "interaction", steepfile: project.steepfile_path)
42
- signature_worker = Server::WorkerProcess.spawn_worker(:signature, name: "signature", steepfile: project.steepfile_path)
43
- code_workers = Server::WorkerProcess.spawn_code_workers(steepfile: project.steepfile_path)
42
+ typecheck_workers = Server::WorkerProcess.spawn_typecheck_workers(steepfile: project.steepfile_path, args: dirs.map(&:to_s), count: jobs_count)
44
43
 
45
44
  master = Server::Master.new(
46
45
  project: project,
47
46
  reader: server_reader,
48
47
  writer: server_writer,
49
- interaction_worker: interaction_worker,
50
- signature_worker: signature_worker,
51
- code_workers: code_workers
48
+ interaction_worker: nil,
49
+ typecheck_workers: typecheck_workers
52
50
  )
51
+ master.typecheck_automatically = false
52
+ master.commandline_args.push(*dirs)
53
53
 
54
54
  main_thread = Thread.start do
55
55
  master.start()
56
56
  end
57
57
  main_thread.abort_on_exception = true
58
58
 
59
- client_writer.write(method: "initialize", id: 0)
59
+ initialize_id = request_id()
60
+ client_writer.write(method: "initialize", id: initialize_id)
61
+ wait_for_response_id(reader: client_reader, id: initialize_id)
60
62
 
61
63
  Steep.logger.info "Watching #{dirs.join(", ")}..."
62
- listener = Listen.to(*dirs.map(&:to_s)) do |modified, added, removed|
64
+
65
+ watch_paths = dirs.map do |dir|
66
+ case
67
+ when dir.directory?
68
+ dir.realpath
69
+ when dir.file?
70
+ dir.parent.realpath
71
+ else
72
+ dir
73
+ end
74
+ end
75
+
76
+ dir_paths = Set.new(dirs.select(&:directory?).map(&:realpath))
77
+ file_paths = Set.new(dirs.select(&:file?).map(&:realpath))
78
+
79
+ listener = Listen.to(*watch_paths.map(&:to_s)) do |modified, added, removed|
63
80
  stdout.puts Rainbow("🔬 Type checking updated files...").bold
64
81
 
65
82
  version = Time.now.to_i
@@ -67,43 +84,39 @@ module Steep
67
84
  Steep.logger.info "Received file system updates: modified=[#{modified.join(",")}], added=[#{added.join(",")}], removed=[#{removed.join(",")}]"
68
85
 
69
86
  (modified + added).each do |path|
70
- client_writer.write(
71
- method: "textDocument/didChange",
72
- params: {
73
- textDocument: {
74
- uri: "file://#{path}",
75
- version: version
76
- },
77
- contentChanges: [
78
- {
79
- text: Pathname(path).read
80
- }
81
- ]
82
- }
83
- )
87
+ p = Pathname(path)
88
+ if watching?(p, files: file_paths, dirs: dir_paths)
89
+ client_writer.write(
90
+ method: "textDocument/didChange",
91
+ params: {
92
+ textDocument: { uri: "file://#{path}", version: version },
93
+ contentChanges: [{ text: p.read }]
94
+ }
95
+ )
96
+ end
84
97
  end
85
98
 
86
99
  removed.each do |path|
87
- client_writer.write(
88
- method: "textDocument/didChange",
89
- params: {
90
- textDocument: {
91
- uri: "file://#{path}",
92
- version: version
93
- },
94
- contentChanges: [
95
- {
96
- text: ""
97
- }
98
- ]
99
- }
100
- )
100
+ if watching?(p, files: file_paths, dirs: dir_paths)
101
+ client_writer.write(
102
+ method: "textDocument/didChange",
103
+ params: {
104
+ textDocument: { uri: "file://#{path}", version: version },
105
+ contentChanges: [{ text: "" }]
106
+ }
107
+ )
108
+ end
101
109
  end
102
110
  end
111
+
112
+ client_writer.write(method: "$/typecheck", params: { guid: nil })
103
113
  end.tap(&:start)
104
114
 
105
115
  begin
106
116
  stdout.puts Rainbow("👀 Watching directories, Ctrl-C to stop.").bold
117
+
118
+ client_writer.write(method: "$/typecheck", params: { guid: nil })
119
+
107
120
  client_reader.read do |response|
108
121
  case response[:method]
109
122
  when "textDocument/publishDiagnostics"
@@ -117,6 +130,7 @@ module Steep
117
130
  unless diagnostics.empty?
118
131
  diagnostics.each do |diagnostic|
119
132
  printer.print(diagnostic)
133
+ stdout.flush
120
134
  end
121
135
  end
122
136
  when "window/showMessage"
@@ -128,16 +142,8 @@ module Steep
128
142
  end
129
143
  end
130
144
  rescue Interrupt
131
- shutdown_id = -1
132
145
  stdout.puts "Shutting down workers..."
133
- client_writer.write({ method: :shutdown, id: shutdown_id })
134
- client_reader.read do |response|
135
- if response[:id] == shutdown_id
136
- break
137
- end
138
- end
139
- client_writer.write({ method: :exit })
140
- client_writer.io.close()
146
+ shutdown_exit(reader: client_reader, writer: client_writer)
141
147
  end
142
148
 
143
149
  listener.stop
@@ -7,6 +7,9 @@ module Steep
7
7
  attr_accessor :worker_type
8
8
  attr_accessor :worker_name
9
9
  attr_accessor :delay_shutdown
10
+ attr_accessor :max_index
11
+ attr_accessor :index
12
+ attr_accessor :commandline_args
10
13
 
11
14
  include Utils::DriverHelper
12
15
 
@@ -14,24 +17,24 @@ module Steep
14
17
  @stdout = stdout
15
18
  @stderr = stderr
16
19
  @stdin = stdin
20
+ @commandline_args = []
17
21
  end
18
22
 
19
23
  def run()
20
24
  Steep.logger.tagged("#{worker_type}:#{worker_name}") do
21
25
  project = load_config()
22
26
 
23
- loader = Project::FileLoader.new(project: project)
24
- loader.load_sources([])
25
- loader.load_signatures()
26
-
27
27
  reader = LanguageServer::Protocol::Transport::Io::Reader.new(stdin)
28
28
  writer = LanguageServer::Protocol::Transport::Io::Writer.new(stdout)
29
29
 
30
30
  worker = case worker_type
31
- when :code
32
- Server::CodeWorker.new(project: project, reader: reader, writer: writer)
33
- when :signature
34
- Server::SignatureWorker.new(project: project, reader: reader, writer: writer)
31
+ when :typecheck
32
+ assignment = Services::PathAssignment.new(max_index: max_index, index: index)
33
+ Server::TypeCheckWorker.new(project: project,
34
+ reader: reader,
35
+ writer: writer,
36
+ assignment: assignment,
37
+ commandline_args: commandline_args)
35
38
  when :interaction
36
39
  Server::InteractionWorker.new(project: project, reader: reader, writer: writer)
37
40
  else
@@ -0,0 +1,159 @@
1
+ module Steep
2
+ class Expectations
3
+ class TestResult
4
+ attr_reader :path
5
+ attr_reader :expectation
6
+ attr_reader :actual
7
+
8
+ def initialize(path:, expectation:, actual:)
9
+ @path = path
10
+ @expectation = expectation
11
+ @actual = actual
12
+ end
13
+
14
+ def empty?
15
+ actual.empty?
16
+ end
17
+
18
+ def satisfied?
19
+ unexpected_diagnostics.empty? && missing_diagnostics.empty?
20
+ end
21
+
22
+ def each_diagnostics
23
+ if block_given?
24
+ expected_set = Set.new(expectation)
25
+ actual_set = Set.new(actual)
26
+
27
+ (expected_set + actual_set).sort_by {|a| Expectations.sort_key(a) }.each do |lsp|
28
+ case
29
+ when expected_set.include?(lsp) && actual_set.include?(lsp)
30
+ yield :expected, lsp
31
+ when expected_set.include?(lsp)
32
+ yield :missing, lsp
33
+ when actual_set.include?(lsp)
34
+ yield :unexpected, lsp
35
+ end
36
+ end
37
+ else
38
+ enum_for :each_diagnostics
39
+ end
40
+ end
41
+
42
+ def expected_diagnostics
43
+ each_diagnostics.select {|type, _| type == :expected }.map {|_, diag| diag }
44
+ end
45
+
46
+ def unexpected_diagnostics
47
+ each_diagnostics.select {|type, _| type == :unexpected }.map {|_, diag| diag }
48
+ end
49
+
50
+ def missing_diagnostics
51
+ each_diagnostics.select {|type, _| type == :missing }.map {|_, diag| diag }
52
+ end
53
+ end
54
+
55
+ LSP = LanguageServer::Protocol
56
+
57
+ attr_reader :diagnostics
58
+
59
+ def self.sort_key(hash)
60
+ [
61
+ hash.dig(:range, :start, :line),
62
+ hash.dig(:range, :start, :character),
63
+ hash.dig(:range, :end, :line),
64
+ hash.dig(:range, :end, :character),
65
+ hash[:code],
66
+ hash[:severity],
67
+ hash[:message]
68
+ ]
69
+ end
70
+
71
+ def initialize()
72
+ @diagnostics = {}
73
+ end
74
+
75
+ def test(path:, diagnostics:)
76
+ TestResult.new(path: path, expectation: self.diagnostics[path] || [], actual: diagnostics)
77
+ end
78
+
79
+ def self.empty
80
+ new()
81
+ end
82
+
83
+ def to_yaml
84
+ array = []
85
+
86
+ diagnostics.each_key.sort.each do |key|
87
+ ds = diagnostics[key]
88
+ array << {
89
+ "file" => key.to_s,
90
+ 'diagnostics' => ds.sort_by {|hash| Expectations.sort_key(hash) }
91
+ .map { |d| Expectations.lsp_to_hash(d) }
92
+ }
93
+ end
94
+
95
+ YAML.dump(array)
96
+ end
97
+
98
+ def self.load(path:, content:)
99
+ expectations = new()
100
+
101
+ YAML.load(content, filename: path.to_s).each do |entry|
102
+ file = Pathname(entry["file"])
103
+ expectations.diagnostics[file] = entry["diagnostics"]
104
+ .map {|hash| hash_to_lsp(hash) }
105
+ .sort_by! {|h| sort_key(h) }
106
+ end
107
+
108
+ expectations
109
+ end
110
+
111
+ # Translate hash to LSP Diagnostic message
112
+ def self.hash_to_lsp(hash)
113
+ {
114
+ range: {
115
+ start: {
116
+ line: hash.dig("range", "start", "line") - 1,
117
+ character: hash.dig("range", "start", "character")
118
+ },
119
+ end: {
120
+ line: hash.dig("range", "end", "line") - 1,
121
+ character: hash.dig("range", "end", "character")
122
+ }
123
+ },
124
+ severity: {
125
+ "ERROR" => LSP::Constant::DiagnosticSeverity::ERROR,
126
+ "WARNING" => LSP::Constant::DiagnosticSeverity::WARNING,
127
+ "INFORMATION" => LSP::Constant::DiagnosticSeverity::INFORMATION,
128
+ "HINT" => LSP::Constant::DiagnosticSeverity::HINT
129
+ }[hash["severity"] || "ERROR"],
130
+ message: hash["message"],
131
+ code: hash["code"]
132
+ }
133
+ end
134
+
135
+ # Translate LSP diagnostic message to hash
136
+ def self.lsp_to_hash(lsp)
137
+ {
138
+ "range" => {
139
+ "start" => {
140
+ "line" => lsp.dig(:range, :start, :line) + 1,
141
+ "character" => lsp.dig(:range, :start, :character)
142
+ },
143
+ "end" => {
144
+ "line" => lsp.dig(:range, :end, :line) + 1,
145
+ "character" => lsp.dig(:range, :end, :character)
146
+ }
147
+ },
148
+ "severity" => {
149
+ LSP::Constant::DiagnosticSeverity::ERROR => "ERROR",
150
+ LSP::Constant::DiagnosticSeverity::WARNING => "WARNING",
151
+ LSP::Constant::DiagnosticSeverity::INFORMATION => "INFORMATION",
152
+ LSP::Constant::DiagnosticSeverity::HINT => "HINT"
153
+ }[lsp[:severity] || LSP::Constant::DiagnosticSeverity::ERROR],
154
+ "message" => lsp[:message],
155
+ "code" => lsp[:code]
156
+ }
157
+ end
158
+ end
159
+ end