steep 2.1.0.dev.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9985b9a834986497b037bbe836f6646aad30fcc1dfa33cf187a3f6aea572d5c6
4
- data.tar.gz: a3ef68064767f01a511e48b02598f4dffa6c403554898b59a76762a732aaa448
3
+ metadata.gz: 3836d60ec7ec072655011df06539859dbbca90170c94a15a6f636537c486eba4
4
+ data.tar.gz: '01394e50e3759284559f635e5f2ef34d79a5b4716a6bbebbf07c383cffd14c41'
5
5
  SHA512:
6
- metadata.gz: '018023551867526507d0b4a490a871c7b06e7bce17aedc87a1e6622d21fc5de212530fd34b94b4fc81fe45e618312433c5a4426969a27bd2144a9a84ee40c874'
7
- data.tar.gz: 5d5c55850108f163260d34f0745e684efa15c922d22a9b417ac41272757c78b579656229ecf191d8e14e33b39cae015cc8fc876d8f4ccdd1fba99045e467a725
6
+ metadata.gz: 66603b9a15c9a6a5f34c1812969d32967d2755c60fe5470d225e13426bfec93f2ff6174a6675c15278fe99fe7d2fec17ae5dbf32d6ab4d9abc6c8bd96ba14166
7
+ data.tar.gz: 0c41cbcdf631c3d5528681b990cad4f36f1dce2ae029aa97ea6e6bbb6199b7af890470c4ca9358cd5152f267cf7f1797d10482e5d5f1e667ae2dce2434fd181f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 2.1.0 (2026-08-27)
4
+
5
+ Steep 2.1 opens the running server to commands other than the editor's. `steep langserver` now binds
6
+ the per-project UNIX socket that `steep server` serves, so `steep check` and `steep query` reach the
7
+ language server the editor is already running instead of needing a daemon of their own, and the new
8
+ `steep query diagnostics` reads the diagnostics that server has already computed rather than starting
9
+ a type check of its own. Both are aimed at tools that edit files on disk and want fast feedback,
10
+ coding agents in particular.
11
+
12
+ The type checker gains support for Ruby 3.4's `it` block parameter, and RBS is updated to 4.2.0.
13
+
14
+ ### Type checker core
15
+
16
+ * Infer tuple types for array literals against pair interface hints like `Hash::_Pair` ([#2253](https://github.com/soutaro/steep/pull/2253))
17
+ * Accept a type alias as the type of a lambda's block parameter ([#2263](https://github.com/soutaro/steep/pull/2263))
18
+ * Build expensive log tags only when something is logged ([#2271](https://github.com/soutaro/steep/pull/2271))
19
+ * Accept block-pass arguments for optional blocks ([#2261](https://github.com/soutaro/steep/pull/2261))
20
+ * feat: support the `it` block parameter ([#2238](https://github.com/soutaro/steep/pull/2238))
21
+ * Fix `UnreachableBranch` on `==` against literal supertype (`Symbol` / `Integer` / `String`) ([#2223](https://github.com/soutaro/steep/pull/2223))
22
+
23
+ ### Commandline tool
24
+
25
+ * Stop `steep query definition` from hanging on inline sources ([#2276](https://github.com/soutaro/steep/pull/2276))
26
+ * Serve steep query/check through the language server's UNIX socket ([#2273](https://github.com/soutaro/steep/pull/2273))
27
+ * Add `steep query diagnostics` ([#2274](https://github.com/soutaro/steep/pull/2274))
28
+ * Point the Steepfile template, README and CLAUDE.md at rbs collection ([#2269](https://github.com/soutaro/steep/pull/2269))
29
+
30
+ ### Miscellaneous
31
+
32
+ * Update RBS to 4.2.0 ([#2277](https://github.com/soutaro/steep/pull/2277))
33
+ * Fix `bin/steep-check.rb` for the current API ([#2270](https://github.com/soutaro/steep/pull/2270))
34
+ * Update RBS to 4.2.0.pre.1 ([#2267](https://github.com/soutaro/steep/pull/2267))
35
+ * Fix Ruby warnings printed while running tests ([#2266](https://github.com/soutaro/steep/pull/2266))
36
+ * Run test subprocesses outside the parent Bundler env ([#2225](https://github.com/soutaro/steep/pull/2225))
37
+
3
38
  ## 2.0.0 (2026-04-14)
4
39
 
5
40
  ### Summary
data/CLAUDE.md CHANGED
@@ -98,8 +98,8 @@ bundle exec rake build
98
98
  **Steepfile**: Defines type checking targets
99
99
  - `check`: Directories to type check
100
100
  - `signature`: RBS signature directories
101
- - `library`: Standard library dependencies
102
- - `collection_config`: External RBS definitions
101
+ - `library`: RBSs that rbs collection doesn't manage
102
+ - `collection_config`: Path to `rbs_collection.yaml` (the project root one is read by default)
103
103
  - Multiple targets supported (app, test, bin)
104
104
 
105
105
  **RBS Collection**: Manages external type definitions via `rbs_collection.steep.yaml`
data/README.md CHANGED
@@ -29,11 +29,11 @@ Edit the `Steepfile`:
29
29
  target :app do
30
30
  check "lib"
31
31
  signature "sig"
32
-
33
- library "pathname"
34
32
  end
35
33
  ```
36
34
 
35
+ RBSs for the gems in `Gemfile.lock` are loaded via [rbs collection](https://github.com/ruby/rbs/blob/master/docs/collection.md), which you set up with `rbs collection init` and `rbs collection install`. Add a `library` call only for an RBS that rbs collection doesn't manage.
36
+
37
37
  ### 1. Declare Types
38
38
 
39
39
  Declare types in `.rbs` files in `sig` directory.
data/bin/steep-check.rb CHANGED
@@ -67,7 +67,7 @@ class Command
67
67
  target.options.load_collection_lock
68
68
 
69
69
  signature_service = Steep::Project::Target.construct_env_loader(options: target.options, project: project).yield_self do |loader|
70
- Steep::Services::SignatureService.load_from(loader)
70
+ Steep::Services::SignatureService.load_from(loader, implicitly_returns_nil: target.implicitly_returns_nil)
71
71
  end
72
72
 
73
73
  env = signature_service.latest_env
@@ -76,8 +76,8 @@ class Command
76
76
 
77
77
  signature_files.each do |path, content|
78
78
  buffer = RBS::Buffer.new(name: path.to_s, content: content)
79
- buffer, dirs, decls = RBS::Parser.parse_signature(buffer)
80
- env.add_signature(buffer: buffer, directives: dirs, decls: decls)
79
+ _, dirs, decls = RBS::Parser.parse_signature(buffer)
80
+ env.add_source(RBS::Source::RBS.new(buffer, dirs, decls))
81
81
  new_decls.merge(decls)
82
82
  end
83
83
 
@@ -94,7 +94,7 @@ class Command
94
94
 
95
95
  definition_builder = RBS::DefinitionBuilder.new(env: env)
96
96
  factory = AST::Types::Factory.new(builder: definition_builder)
97
- builder = Interface::Builder.new(factory)
97
+ builder = Interface::Builder.new(factory, implicitly_returns_nil: target.implicitly_returns_nil)
98
98
  subtyping = Subtyping::Check.new(builder: builder)
99
99
 
100
100
  typings = {}
@@ -4,7 +4,6 @@ module Steep
4
4
  module Helper
5
5
  module ChildrenLevel
6
6
  def level_of_children(children)
7
- levels = children.map(&:level)
8
7
  children.map(&:level).sort {|a, b| (b.size <=> a.size) || 0 }.inject() do |a, b|
9
8
  a.zip(b).map do |x, y|
10
9
  if x && y
@@ -88,7 +88,7 @@ module Steep
88
88
  def map_type(&block)
89
89
  args = self.args.map(&block)
90
90
 
91
- _ = self.class.new(name: self.name, args: self.args)
91
+ _ = self.class.new(name: self.name, args: args)
92
92
  end
93
93
  end
94
94
 
data/lib/steep/cli.rb CHANGED
@@ -355,6 +355,9 @@ Options:
355
355
  BANNER
356
356
  handle_steepfile_option(opts, command)
357
357
  opts.on("--refork") { command.refork = true }
358
+ opts.on("--[no-]command-socket", "Accept `steep query`/`steep check` connections on a UNIX socket (default: true)") do |v|
359
+ command.command_socket = v ? true : false
360
+ end
358
361
  handle_jobs_option command.jobs_option, opts
359
362
  handle_logging_options opts
360
363
  end.parse!(argv)
@@ -618,8 +621,9 @@ BANNER
618
621
  The user interface and output format may change without deprecation.
619
622
 
620
623
  Available subcommands:
621
- hover Get hover information (type, documentation) for a position
622
- definition Get the definition(s) of a class, type alias, constant, or method name
624
+ hover Get hover information (type, documentation) for a position
625
+ definition Get the definition(s) of a class, type alias, constant, or method name
626
+ diagnostics Get the type check diagnostics the server has computed
623
627
 
624
628
  Options:
625
629
  --help Show this help message
@@ -628,6 +632,7 @@ BANNER
628
632
  steep query hover lib/foo.rb:10:5
629
633
  steep query definition RBS::Location
630
634
  steep query definition RBS::Parser.parse_signature
635
+ steep query diagnostics lib/foo.rb sig/foo.rbs
631
636
  HELP
632
637
  return 0
633
638
  end
@@ -714,9 +719,33 @@ BANNER
714
719
  end
715
720
 
716
721
  Drivers::Query.new(stdout: stdout, stderr: stderr).run_definition(names: argv.dup)
722
+ when "diagnostics"
723
+ OptionParser.new do |opts|
724
+ opts.banner = <<BANNER
725
+ Usage: steep query diagnostics [options] [FILE ...]
726
+
727
+ Description:
728
+ Get the type check diagnostics the server has computed.
729
+ Connects to the running Steep server, reloads files changed on disk, waits for the
730
+ type checking to finish, and prints the diagnostics as JSONL (one JSON object per
731
+ line for each file).
732
+
733
+ When FILEs are given, only the diagnostics of those files are printed.
734
+ `"diagnostics": null` means the server has not type checked the file yet.
735
+
736
+ Note:
737
+ This is an experimental command.
738
+ The user interface and output format may change without deprecation.
739
+
740
+ Options:
741
+ BANNER
742
+ handle_logging_options opts
743
+ end.parse!(argv)
744
+
745
+ Drivers::Query.new(stdout: stdout, stderr: stderr).run_diagnostics(paths: argv.dup)
717
746
  else
718
747
  stderr.puts "Unknown query subcommand: #{subcommand}"
719
- stderr.puts " available subcommands: hover, definition"
748
+ stderr.puts " available subcommands: hover, definition, diagnostics"
720
749
  1
721
750
  end
722
751
  end
data/lib/steep/daemon.rb CHANGED
@@ -43,18 +43,22 @@ module Steep
43
43
  false
44
44
  end
45
45
 
46
+ # Returns true when something is serving the socket, whether it is a daemon or a language server
47
+ #
48
+ # The socket is the only reliable signal: a language server records its pid elsewhere, so
49
+ # requiring `pid_path` here would hide it from `steep check` and `steep query`.
50
+ #
46
51
  def running?
47
- return false unless File.exist?(pid_path) && File.exist?(socket_path)
52
+ return false unless File.exist?(socket_path)
48
53
 
49
- pid = File.read(pid_path).to_i
50
- Process.kill(0, pid)
51
54
  socket = UNIXSocket.new(socket_path)
52
55
  socket.close
53
56
  true
54
- rescue Errno::ESRCH, Errno::ENOENT, Errno::ECONNREFUSED, Errno::ENOTSOCK
57
+ rescue Errno::ENOENT, Errno::ECONNREFUSED, Errno::ENOTSOCK
55
58
  false
56
59
  end
57
60
 
61
+
58
62
  def cleanup
59
63
  [socket_path, pid_path].each do |path|
60
64
  File.delete(path)
@@ -64,8 +68,13 @@ module Steep
64
68
  end
65
69
 
66
70
  def start(stderr:)
71
+ if pid = live_pid(pid_path)
72
+ stderr.puts "Steep server already running (PID: #{pid})"
73
+ return true
74
+ end
75
+
67
76
  if running?
68
- stderr.puts "Steep server already running (PID: #{File.read(pid_path).strip})"
77
+ stderr.puts "Another process is already serving this project (e.g. `steep langserver`)"
69
78
  return true
70
79
  end
71
80
 
@@ -106,7 +115,12 @@ module Steep
106
115
 
107
116
  def stop(stderr:)
108
117
  unless File.exist?(pid_path)
109
- stderr.puts "Steep server is not running"
118
+ if running?
119
+ stderr.puts "Steep server is not running. This project is served by another process"
120
+ stderr.puts "(e.g. `steep langserver`), which this command does not stop."
121
+ else
122
+ stderr.puts "Steep server is not running"
123
+ end
110
124
  return
111
125
  end
112
126
 
@@ -129,13 +143,29 @@ module Steep
129
143
  end
130
144
  cleanup
131
145
  rescue Errno::ESRCH
132
- cleanup
133
- stderr.puts "Steep server was not running (cleaned up stale files)"
146
+ # The pid file was stale. The socket may still belong to another live server,
147
+ # which `cleanup` would break by deleting it.
148
+ if running?
149
+ begin
150
+ File.delete(pid_path)
151
+ rescue Errno::ENOENT
152
+ # Already deleted
153
+ end
154
+ stderr.puts "Removed a stale pid file. This project is served by another process."
155
+ else
156
+ cleanup
157
+ stderr.puts "Steep server was not running (cleaned up stale files)"
158
+ end
134
159
  end
135
160
 
136
161
  def status(stderr:)
137
162
  if running?
138
- pid = File.read(pid_path).to_i
163
+ unless pid = live_pid(pid_path)
164
+ stderr.puts "This project is served by another process (e.g. `steep langserver`)"
165
+ stderr.puts " Socket: #{socket_path}"
166
+ return
167
+ end
168
+
139
169
  stderr.puts "Steep server running (PID: #{pid})"
140
170
  stderr.puts " Socket: #{socket_path}"
141
171
  stderr.puts " Log: #{log_path}"
@@ -167,6 +197,16 @@ module Steep
167
197
 
168
198
  private
169
199
 
200
+ # The pid recorded in `path`, when the file exists and that process is alive
201
+ def live_pid(path)
202
+ pid = File.read(path).to_i
203
+ return nil unless pid > 0
204
+ Process.kill(0, pid)
205
+ pid
206
+ rescue Errno::ESRCH, Errno::ENOENT
207
+ nil
208
+ end
209
+
170
210
  def run_server(stderr:)
171
211
  project = load_project
172
212
  server = Server.new(config: config, project: project, stderr:)
@@ -19,8 +19,9 @@ module Steep
19
19
  # check "app/models/**/*.rb" # Glob
20
20
  # # ignore "lib/templates/*.rb"
21
21
  #
22
- # # library "pathname" # Standard libraries
23
- # # library "strong_json" # Gems
22
+ # # RBSs for the gems in Gemfile.lock are loaded via rbs collection automatically.
23
+ # # Set it up with `rbs collection init` and `rbs collection install`.
24
+ # # library "monitor" # Load an RBS that rbs collection doesn't manage
24
25
  #
25
26
  # # configure_code_diagnostics(D::Ruby.default) # `default` diagnostics setting (applies by default)
26
27
  # # configure_code_diagnostics(D::Ruby.strict) # `strict` diagnostics setting
@@ -38,7 +39,7 @@ module Steep
38
39
  #
39
40
  # configure_code_diagnostics(D::Ruby.lenient) # Weak type checking for test code
40
41
  #
41
- # # library "pathname" # Standard libraries
42
+ # # library "monitor" # Load an RBS that rbs collection doesn't manage
42
43
  # end
43
44
  EOF
44
45
 
@@ -9,6 +9,7 @@ module Steep
9
9
  attr_reader :type_check_thread
10
10
  attr_reader :jobs_option
11
11
  attr_accessor :refork
12
+ attr_accessor :command_socket
12
13
 
13
14
  include Utils::DriverHelper
14
15
 
@@ -20,6 +21,7 @@ module Steep
20
21
  @type_check_queue = Queue.new
21
22
  @jobs_option = Utils::JobsOption.new(jobs_count_modifier: -1)
22
23
  @refork = false
24
+ @command_socket = true
23
25
  end
24
26
 
25
27
  def writer
@@ -50,10 +52,40 @@ module Steep
50
52
  )
51
53
  master.typecheck_automatically = true
52
54
 
53
- master.start()
55
+ socket = start_command_socket(master)
56
+
57
+ begin
58
+ master.start()
59
+ ensure
60
+ socket&.stop
61
+ end
54
62
 
55
63
  0
56
64
  end
65
+
66
+ # Starts accepting `steep query`/`steep check` connections on the UNIX socket
67
+ #
68
+ # Returns `nil` when the command socket is disabled, is not supported on the platform,
69
+ # or is already served by another process.
70
+ #
71
+ def start_command_socket(master)
72
+ return nil unless command_socket
73
+
74
+ configuration = Daemon::Configuration.new(base_dir: project.base_dir.to_s)
75
+ socket = Server::CommandSocket.new(master: master, configuration: configuration)
76
+
77
+ if socket.start
78
+ stderr.puts "Steep command socket is ready: #{configuration.socket_path}"
79
+ socket
80
+ else
81
+ stderr.puts "Steep command socket is not available: #{configuration.socket_path}"
82
+ nil
83
+ end
84
+ rescue NotImplementedError, StandardError => error
85
+ Steep.logger.error { "Failed to start command socket: #{error.inspect}" }
86
+ stderr.puts "Failed to start Steep command socket: #{error.message}"
87
+ nil
88
+ end
57
89
  end
58
90
  end
59
91
  end
@@ -15,7 +15,7 @@ module Steep
15
15
  # @rbs return: Integer
16
16
  def run_hover(locations:)
17
17
  unless Daemon.running?
18
- stderr.puts "Error: Steep daemon is not running. Start it with `steep server start`."
18
+ stderr.puts "Error: Steep server is not running. Start it with `steep server start` or `steep langserver`."
19
19
  return 1
20
20
  end
21
21
 
@@ -51,7 +51,7 @@ module Steep
51
51
  # @rbs return: Integer
52
52
  def run_definition(names:)
53
53
  unless Daemon.running?
54
- stderr.puts "Error: Steep daemon is not running. Start it with `steep server start`."
54
+ stderr.puts "Error: Steep server is not running. Start it with `steep server start` or `steep langserver`."
55
55
  return 1
56
56
  end
57
57
 
@@ -72,6 +72,44 @@ module Steep
72
72
  1
73
73
  end
74
74
 
75
+ def run_diagnostics(paths:)
76
+ unless Daemon.running?
77
+ stderr.puts "Error: Steep server is not running. Start it with `steep server start` or `steep langserver`."
78
+ return 1
79
+ end
80
+
81
+ absolute_paths = [] #: Array[Pathname]
82
+ paths.each do |path|
83
+ pathname = Pathname(path)
84
+ pathname = Pathname.pwd + pathname unless pathname.absolute?
85
+ absolute_paths << pathname
86
+ end
87
+
88
+ request = {
89
+ id: SecureRandom.uuid,
90
+ method: Server::CustomMethods::Query__Diagnostics::METHOD,
91
+ params: { paths: absolute_paths.empty? ? nil : absolute_paths.map(&:to_s) }
92
+ }
93
+
94
+ result = send_request(request) #: Array[{ uri: String, diagnostics: Array[untyped]? }]?
95
+
96
+ (result || []).each do |entry|
97
+ path = PathHelper.to_pathname(entry[:uri]) || Pathname(entry[:uri])
98
+ begin
99
+ path = path.relative_path_from(Pathname.pwd)
100
+ rescue ArgumentError
101
+ # Keep the absolute path of files outside of the current directory
102
+ end
103
+
104
+ stdout.puts JSON.generate({ path: path.to_s, diagnostics: entry[:diagnostics] })
105
+ end
106
+
107
+ 0
108
+ rescue Errno::ECONNREFUSED, Errno::ENOENT => e
109
+ stderr.puts "Error: Failed to connect to Steep server: #{e.message}"
110
+ 1
111
+ end
112
+
75
113
  private
76
114
 
77
115
  def to_absolute_path(path)
@@ -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
@@ -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",