steep 2.0.0 → 2.1.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.
@@ -65,7 +65,7 @@ module Steep
65
65
  end
66
66
 
67
67
  def shape(type, config)
68
- Steep.logger.tagged "shape(#{type})" do
68
+ Steep.logger.tagged(-> { "shape(#{type})" }) do
69
69
  if shape = raw_shape(type, config)
70
70
  # Optimization that skips unnecessary substitution
71
71
  if type.free_variables.include?(AST::Types::Self.instance)
@@ -82,7 +82,7 @@ module Steep
82
82
  end
83
83
 
84
84
  def fetch_cache(cache, key)
85
- if cache.key?(key) # steep:ignore ArgumentTypeMismatch
85
+ if cache.key?(key)
86
86
  return cache.fetch(key)
87
87
  end
88
88
 
@@ -277,7 +277,7 @@ module Steep
277
277
  definition = factory.definition_builder.build_singleton(type_name)
278
278
 
279
279
  definition.methods.each do |name, method|
280
- Steep.logger.tagged "method = #{type_name}.#{name}" do
280
+ Steep.logger.tagged(-> { "method = #{type_name}.#{name}" }) do
281
281
  overloads = method.defs.map do |type_def|
282
282
  method_name = method_name_for(type_def, name)
283
283
  method_type = factory.method_type(type_def.type)
@@ -309,7 +309,7 @@ module Steep
309
309
  definition or raise
310
310
 
311
311
  definition.methods.each do |name, method|
312
- Steep.logger.tagged "method = #{type_name}##{name}" do
312
+ Steep.logger.tagged(-> { "method = #{type_name}##{name}" }) do
313
313
  overloads = method.defs.map do |type_def|
314
314
  method_name = method_name_for(type_def, name)
315
315
  method_type = factory.method_type(type_def.type)
@@ -592,11 +592,31 @@ module Steep
592
592
  )
593
593
  end
594
594
 
595
+ to_ary_entry = Shape::Entry.new(
596
+ method_name: :to_ary,
597
+ private_method: false,
598
+ overloads: [
599
+ Shape::MethodOverload.new(
600
+ MethodType.new(
601
+ type_params: [],
602
+ type: Function.new(
603
+ params: Function::Params.empty,
604
+ return_type: tuple,
605
+ location: nil
606
+ ),
607
+ block: nil
608
+ ),
609
+ []
610
+ )
611
+ ]
612
+ )
613
+
595
614
  shape.methods[:[]] = aref_entry
596
615
  shape.methods[:[]=] = aref_update_entry
597
616
  shape.methods[:fetch] = fetch_entry
598
617
  shape.methods[:first] = first_entry
599
618
  shape.methods[:last] = last_entry
619
+ shape.methods[:to_ary] = to_ary_entry
600
620
 
601
621
  shape
602
622
  end
@@ -220,7 +220,7 @@ module Steep
220
220
 
221
221
  def private_send?(node)
222
222
  case node.type
223
- when :block, :numblock
223
+ when :block, :numblock, :itblock
224
224
  private_send?(node.children[0])
225
225
  when :send, :csend
226
226
  receiver, = deconstruct_send_node!(node)
@@ -243,7 +243,7 @@ module Steep
243
243
  when :send, :csend, :super
244
244
  if block_node
245
245
  case block_node.type
246
- when :block, :numblock
246
+ when :block, :numblock, :itblock
247
247
  if send_node.equal?(block_node.children[0])
248
248
  return [send_node, block_node]
249
249
  end
@@ -56,6 +56,21 @@ module Steep
56
56
  handle_job(job)
57
57
  rescue => exn
58
58
  Steep.log_error exn
59
+
60
+ # Jobs that carry an `id` answer a request. Reply with an error response, or the
61
+ # client would wait for a response that never arrives.
62
+ if job.respond_to?(:id) && (id = job.id)
63
+ writer.write(
64
+ {
65
+ id: id,
66
+ error: {
67
+ code: LSP::Constant::ErrorCodes::INTERNAL_ERROR,
68
+ message: "Unexpected error: #{exn.message} (#{exn.class})"
69
+ }
70
+ }
71
+ )
72
+ end
73
+
59
74
  writer.write(
60
75
  {
61
76
  method: "window/showMessage",
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Steep
4
+ module Server
5
+ # Accepts connections on a UNIX socket and forwards the received requests to the master
6
+ #
7
+ # Started by `steep langserver` so that CLI commands like `steep query` and `steep check` can
8
+ # talk to the language server the IDE is running, instead of a standalone daemon process.
9
+ #
10
+ class CommandSocket
11
+ LSP = LanguageServer::Protocol
12
+
13
+ # A connection from a CLI client
14
+ #
15
+ # `#write` never raises even if the client is disconnected, because it may be called from
16
+ # the master's write thread, which should keep running for other clients.
17
+ #
18
+ class Session
19
+ attr_reader :socket
20
+
21
+ def initialize(socket)
22
+ @socket = socket
23
+ @writer = LSP::Transport::Io::Writer.new(socket)
24
+ @mutex = Mutex.new
25
+ @closed = false
26
+ end
27
+
28
+ def write(message)
29
+ @mutex.synchronize do
30
+ return if @closed
31
+ @writer.write(message)
32
+ end
33
+ rescue SystemCallError, IOError
34
+ close()
35
+ end
36
+
37
+ def close
38
+ @mutex.synchronize do
39
+ @closed = true
40
+ end
41
+ begin
42
+ @socket.close
43
+ rescue IOError
44
+ # Already closed
45
+ end
46
+ end
47
+ end
48
+
49
+ attr_reader :master, :configuration
50
+
51
+ def initialize(master:, configuration:)
52
+ @master = master
53
+ @configuration = configuration
54
+ @server = nil
55
+ @accept_thread = nil
56
+ end
57
+
58
+ def socket_path
59
+ configuration.socket_path
60
+ end
61
+
62
+ # Binds the UNIX socket and starts accepting connections in a background thread
63
+ #
64
+ # Returns `false` without binding when another process (a standalone `steep server` daemon or
65
+ # another language server) is already serving on the socket.
66
+ #
67
+ def start
68
+ if File.exist?(socket_path)
69
+ if socket_alive?
70
+ Steep.logger.warn { "Command socket is already served by another process: #{socket_path}" }
71
+ return false
72
+ end
73
+
74
+ unless File.socket?(socket_path)
75
+ Steep.logger.error { "#{socket_path} exists but is not a socket, skipping command socket setup" }
76
+ return false
77
+ end
78
+
79
+ File.delete(socket_path)
80
+ end
81
+
82
+ @server = UNIXServer.new(socket_path)
83
+ File.chmod(0o600, socket_path)
84
+
85
+ @accept_thread = Thread.new do
86
+ Thread.current.abort_on_exception = false
87
+ accept_loop()
88
+ end
89
+
90
+ Steep.logger.info { "Command socket is ready: #{socket_path}" }
91
+
92
+ true
93
+ rescue Errno::EADDRINUSE
94
+ Steep.logger.warn { "Command socket is already served by another process: #{socket_path}" }
95
+ false
96
+ end
97
+
98
+ def stop
99
+ server = @server
100
+ @server = nil
101
+
102
+ if server
103
+ begin
104
+ server.close
105
+ rescue IOError
106
+ # Already closed
107
+ end
108
+
109
+ @accept_thread&.join(1)
110
+
111
+ begin
112
+ File.delete(socket_path)
113
+ rescue Errno::ENOENT
114
+ # Already deleted
115
+ end
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def socket_alive?
122
+ socket = UNIXSocket.new(socket_path)
123
+ socket.close
124
+ true
125
+ rescue SystemCallError
126
+ false
127
+ end
128
+
129
+ def accept_loop
130
+ while server = @server
131
+ socket =
132
+ begin
133
+ server.accept
134
+ rescue IOError, SystemCallError => error
135
+ Steep.logger.info { "Command socket accept loop stopping: #{error.inspect}" }
136
+ break
137
+ end
138
+
139
+ # The loop must survive anything but the accept errors above: it runs with
140
+ # `abort_on_exception` disabled, so an uncaught error would kill the thread
141
+ # silently and every connection after it would sit in the backlog forever.
142
+ begin
143
+ Steep.logger.info { "Command socket accepted a connection" }
144
+
145
+ # `session` is passed as a thread argument: `while` shares its locals across
146
+ # iterations, so a block reading `session` directly would serve the session of a
147
+ # later iteration when the accept loop wins the race against the thread's start,
148
+ # leaving the accepted connection unread.
149
+ Thread.new(Session.new(socket)) do |session|
150
+ Thread.current.abort_on_exception = false
151
+ serve(session)
152
+ end
153
+ rescue StandardError => error
154
+ Steep.logger.error { "Command socket could not serve an accepted connection: #{error.inspect}" }
155
+ begin
156
+ socket.close
157
+ rescue IOError
158
+ # Already closed
159
+ end
160
+ end
161
+ end
162
+ ensure
163
+ Steep.logger.info { "Command socket accept loop finished" }
164
+ end
165
+
166
+ def serve(session)
167
+ reader = LSP::Transport::Io::Reader.new(session.socket)
168
+ reader.read do |message|
169
+ Steep.logger.info { "Command socket received message: method=#{message[:method] || "-"}, id=#{message[:id] || "-"}" }
170
+ master.process_command_socket_message(message, session)
171
+ end
172
+ rescue IOError, SystemCallError => error
173
+ Steep.logger.warn { "Command socket client error: #{error.inspect}" }
174
+ ensure
175
+ Steep.logger.info { "Command socket connection closed" }
176
+ master.finish_command_socket_session(session)
177
+ session.close
178
+ end
179
+ end
180
+ end
181
+ end
@@ -96,6 +96,18 @@ module Steep
96
96
  { id: id, result: result }
97
97
  end
98
98
  end
99
+
100
+ module Query__Diagnostics
101
+ METHOD = "$/steep/query/diagnostics"
102
+
103
+ def self.request(id, params)
104
+ { method: METHOD, id: id, params: params }
105
+ end
106
+
107
+ def self.response(id, result)
108
+ { id: id, result: result }
109
+ end
110
+ end
99
111
  end
100
112
  end
101
113
  end