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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/CLAUDE.md +2 -2
- data/README.md +3 -3
- data/Rakefile +350 -147
- data/bin/steep-check.rb +4 -4
- data/doc/release.md +264 -0
- data/lib/steep/ast/types/helper.rb +0 -1
- data/lib/steep/ast/types/name.rb +1 -1
- data/lib/steep/cli.rb +32 -3
- data/lib/steep/daemon.rb +49 -9
- data/lib/steep/diagnostic/ruby.rb +4 -4
- data/lib/steep/drivers/init.rb +4 -3
- data/lib/steep/drivers/langserver.rb +33 -1
- data/lib/steep/drivers/query.rb +40 -2
- data/lib/steep/interface/builder.rb +24 -4
- data/lib/steep/node_helper.rb +2 -2
- data/lib/steep/server/base_worker.rb +15 -0
- data/lib/steep/server/command_socket.rb +181 -0
- data/lib/steep/server/custom_methods.rb +12 -0
- data/lib/steep/server/master.rb +383 -2
- data/lib/steep/server/type_check_worker.rb +48 -2
- data/lib/steep/services/goto_service.rb +7 -3
- data/lib/steep/services/hover_provider/content.rb +1 -1
- data/lib/steep/services/hover_provider/ruby.rb +1 -1
- data/lib/steep/services/signature_help_provider.rb +1 -1
- data/lib/steep/source.rb +9 -10
- data/lib/steep/subtyping/check.rb +6 -3
- data/lib/steep/tagged_logging.rb +4 -1
- data/lib/steep/type_construction.rb +37 -16
- data/lib/steep/type_inference/case_when.rb +2 -2
- data/lib/steep/type_inference/logic_type_interpreter.rb +6 -3
- data/lib/steep/type_inference/send_args.rb +9 -5
- data/lib/steep/typing.rb +3 -3
- data/lib/steep/version.rb +1 -1
- data/lib/steep.rb +1 -0
- data/steep.gemspec +2 -8
- metadata +7 -5
|
@@ -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)
|
|
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
|
data/lib/steep/node_helper.rb
CHANGED
|
@@ -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
|