steep 0.40.0 → 0.44.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 (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