steep-relaxed 2.0.0.0 → 2.1.0.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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/CLAUDE.md +2 -2
  4. data/README.md +3 -3
  5. data/Rakefile +352 -143
  6. data/Steepfile +2 -0
  7. data/bin/shape_warmup_bench.rb +402 -0
  8. data/bin/steep-check.rb +4 -4
  9. data/bin/subtyping_cache_bench.rb +436 -0
  10. data/bin/typecheck_bench.rb +145 -0
  11. data/doc/release.md +264 -0
  12. data/lib/steep/ast/types/helper.rb +0 -1
  13. data/lib/steep/ast/types/literal.rb +1 -1
  14. data/lib/steep/ast/types/name.rb +1 -1
  15. data/lib/steep/cli.rb +32 -5
  16. data/lib/steep/daemon/server.rb +1 -3
  17. data/lib/steep/daemon.rb +49 -9
  18. data/lib/steep/diagnostic/ruby.rb +4 -4
  19. data/lib/steep/drivers/check.rb +0 -1
  20. data/lib/steep/drivers/checkfile.rb +4 -12
  21. data/lib/steep/drivers/init.rb +4 -3
  22. data/lib/steep/drivers/langserver.rb +34 -5
  23. data/lib/steep/drivers/query.rb +40 -2
  24. data/lib/steep/drivers/stats.rb +0 -1
  25. data/lib/steep/drivers/worker.rb +0 -7
  26. data/lib/steep/interface/builder.rb +194 -28
  27. data/lib/steep/interface/shape.rb +28 -14
  28. data/lib/steep/node_helper.rb +2 -2
  29. data/lib/steep/server/base_worker.rb +27 -30
  30. data/lib/steep/server/change_buffer.rb +0 -32
  31. data/lib/steep/server/command_socket.rb +181 -0
  32. data/lib/steep/server/custom_methods.rb +22 -22
  33. data/lib/steep/server/goto_resolver.rb +200 -0
  34. data/lib/steep/server/interaction_worker.rb +32 -12
  35. data/lib/steep/server/master.rb +709 -261
  36. data/lib/steep/server/rbs_entry_builder.rb +237 -0
  37. data/lib/steep/server/type_check_controller.rb +73 -27
  38. data/lib/steep/server/type_check_database.rb +406 -0
  39. data/lib/steep/server/type_check_worker.rb +118 -352
  40. data/lib/steep/server/worker_process.rb +8 -22
  41. data/lib/steep/services/hover_provider/content.rb +1 -1
  42. data/lib/steep/services/hover_provider/ruby.rb +1 -1
  43. data/lib/steep/services/signature_help_provider.rb +1 -1
  44. data/lib/steep/services/stats_calculator.rb +16 -35
  45. data/lib/steep/services/symbol_provider.rb +191 -0
  46. data/lib/steep/services/type_check_service.rb +10 -21
  47. data/lib/steep/source.rb +9 -10
  48. data/lib/steep/subtyping/cache.rb +16 -4
  49. data/lib/steep/subtyping/check.rb +83 -16
  50. data/lib/steep/subtyping/stats.rb +356 -0
  51. data/lib/steep/tagged_logging.rb +4 -1
  52. data/lib/steep/type_construction.rb +39 -17
  53. data/lib/steep/type_inference/case_when.rb +2 -2
  54. data/lib/steep/type_inference/logic_type_interpreter.rb +6 -3
  55. data/lib/steep/type_inference/send_args.rb +9 -5
  56. data/lib/steep/typing.rb +3 -3
  57. data/lib/steep/version.rb +1 -1
  58. data/lib/steep.rb +6 -1
  59. data/steep-relaxed.gemspec +2 -8
  60. metadata +15 -6
  61. data/lib/steep/services/goto_service.rb +0 -644
data/doc/release.md ADDED
@@ -0,0 +1,264 @@
1
+ # Releasing Steep
2
+
3
+ A release is a pull request and one workflow run. Everything that leaves the
4
+ repository — the tag, the gem, and the GitHub release — is produced by the
5
+ `Release gem` workflow, so nothing has to be built or pushed from a laptop.
6
+
7
+ There are three kinds of release, and they differ in what gets written up:
8
+
9
+ | Version | CHANGELOG section | GitHub release |
10
+ | --- | --- | --- |
11
+ | `X.Y.Z` | The whole cycle since the previous release proper, prereleases included | Published |
12
+ | `X.Y.Z.pre.N` | What changed since `X.Y.Z.pre.N-1` | Published, marked as a prerelease |
13
+ | `X.Y.Z.dev.N` | None | None |
14
+
15
+ `.dev.N` releases are cut from the development line for people who need a change
16
+ early, so they are gems and tags and nothing else.
17
+
18
+ ## Prerequisites
19
+
20
+ Push rights to the `steep` gem on RubyGems are **not** needed: the workflow
21
+ authenticates through a trusted publisher registered for this repository and
22
+ `release.yml`. What is needed is write access to the repository, since that is
23
+ what lets you dispatch the workflow.
24
+
25
+ ## Steps
26
+
27
+ The release pull request in step 1 is merged by a person who has reviewed it. Its merge commit is
28
+ what step 2 dispatches, tags, and pushes to RubyGems, and none of that can be taken back — so
29
+ prepare that pull request and stop there, rather than merging it and carrying on to step 2.
30
+
31
+ The bump that starts a new minor is the only other pull request that sets `Steep::VERSION`. It
32
+ publishes nothing and another bump undoes it, so one opened on an explicit request can go through
33
+ on its own.
34
+
35
+ ### 1. Prepare the release
36
+
37
+ Open a pull request that carries everything the release needs:
38
+
39
+ - `lib/steep/version.rb` — set `Steep::VERSION` to the version being released.
40
+ - `CHANGELOG.md` — add a section for the new version, directly under the `# CHANGELOG` heading.
41
+ Sections are newest first.
42
+
43
+ Label the pull request `skip-changelog`. It carries no change of its own, and without the label it
44
+ shows up in the next release's list.
45
+
46
+ `rake gem:changelog` lists the pull requests merged since the last release, already formatted:
47
+
48
+ ```console
49
+ $ bundle exec rake gem:changelog | pbcopy
50
+ ```
51
+
52
+ Where it starts follows `Steep::VERSION`, so bump the version first: a prerelease starts from the
53
+ latest tag, and a release proper skips the prerelease tags and starts from the previous release
54
+ proper. Pass a version to override it (`rake 'gem:changelog[2.0.0]'`). Only the list goes to
55
+ STDOUT, so it pipes cleanly. Pull requests labeled `skip-changelog` are left out and reported on
56
+ STDERR.
57
+
58
+ Sort the list into the sections below. `rake gem:changelog:json` prints the same pull requests with
59
+ the changed files, labels, and body of each, which is what the sorting is based on.
60
+
61
+ Both tasks reach GitHub through `gh`, which a Claude Code on the web session cannot do. See
62
+ [Assembling the changelog without `gh`](#assembling-the-changelog-without-gh) for how the same list
63
+ is produced there.
64
+
65
+ ```markdown
66
+ ## X.Y.Z (YYYY-MM-DD)
67
+
68
+ ### Type checker core
69
+
70
+ ### Commandline tool
71
+
72
+ ### Language server
73
+
74
+ ### Miscellaneous
75
+ ```
76
+
77
+ The sections always appear in this order; delete the ones that end up empty. One thing scales with
78
+ the size of the release: **summary paragraphs**, above the first section. A patch release usually
79
+ has none, while 2.0.0 opens with a `### Summary` describing its three major features.
80
+
81
+ The date is the day the gem is released, matching the `vX.Y.Z` tag — not the day this pull request
82
+ is opened. Fix it up before step 2 if the pull request sat for a few days.
83
+
84
+ ### 2. Run the `Release gem` workflow
85
+
86
+ Once the pull request is merged, dispatch
87
+ [`release.yml`](../.github/workflows/release.yml) from the Actions tab with two inputs:
88
+
89
+ | Input | Value |
90
+ | --- | --- |
91
+ | `commit` | The full 40-character SHA of the merge commit, taken from the merged pull request |
92
+ | `version` | `X.Y.Z`, without the leading `v` |
93
+
94
+ The ref selector picks which copy of the workflow file runs, not what gets released — leave it on
95
+ `master`. Everything is built from `commit`, so the run is unaffected by whatever lands on `master`
96
+ in the meantime, and a patch release cut from a release branch is dispatched the same way as any
97
+ other: the workflow does not care which branch the commit is on.
98
+
99
+ The two inputs say the same thing twice, once as a commit and once as a name, and the run stops
100
+ before anything is built unless they agree with each other and with the repository:
101
+
102
+ - `commit` has to be a full SHA that some branch contains,
103
+ - `version` has to be the `Steep::VERSION` that commit declares,
104
+ - CHANGELOG.md has to start with a section for `version` (skipped for `.dev.N`, which is not
105
+ written up),
106
+ - `vX.Y.Z` must not exist yet.
107
+
108
+ It then:
109
+
110
+ - builds `steep-X.Y.Z.gem`,
111
+ - checks its metadata: the platform, no C extension, `exe/steep` and `lib/steep.rb` present, and
112
+ none of the development directories shipped,
113
+ - installs the gem the way a user would and type checks a small project with it — one that has to
114
+ pass and one that has to fail — so the executable, the dependencies, and the type checker are
115
+ exercised before anything is published,
116
+ - uploads the gem as an artifact,
117
+ - tags `commit` as `vX.Y.Z` and pushes the tag,
118
+ - pushes the gem to RubyGems through trusted publishing,
119
+ - publishes the GitHub release with the notes from CHANGELOG.md, skipping this last step for
120
+ `.dev.N` versions.
121
+
122
+ The tag is created once the gem is known to build and run, and before anything is published: a
123
+ tag can be deleted, while a version pushed to RubyGems can only be yanked.
124
+
125
+ Checking the `dry_run` box runs everything up to the artifact and stops — no tag, no gem pushed,
126
+ no release — which is how the build is exercised without releasing. `version` still has to match
127
+ the commit, so a dry run is also how a release is rehearsed before it is cut.
128
+
129
+ ## The version on `master`
130
+
131
+ `Steep::VERSION` on `master` is read one of two ways, told apart by how the version ends:
132
+
133
+ | On `master` | Means |
134
+ | --- | --- |
135
+ | `X.Y.0.dev` — a bare `.dev` | `X.Y.0` is being developed |
136
+ | A complete version — `X.Y.Z`, `X.Y.Z.pre.N`, `X.Y.Z.dev.N` | The version *after* the one named is being developed |
137
+
138
+ So `2.0.0` on `master` is not a claim that `master` is 2.0.0. It says 2.0.0 has shipped and what
139
+ comes after it is being worked on. Both become true the moment the release is tagged, so **nothing
140
+ has to be done to `master` after a release**.
141
+
142
+ The bare `X.Y.0.dev` is the exception because it is the one version that names a target rather than
143
+ a predecessor: a new minor is developed towards `X.Y.0` for a long time, before it is known whether
144
+ the next thing to ship is `X.Y.0.pre.1` or `X.Y.0` itself. Setting it is the only version change
145
+ that has to be made deliberately.
146
+
147
+ `rake gem:changelog` reads `Steep::VERSION` too, to decide where the next changelog starts — but
148
+ the version is set to the one being released before the changelog is generated, so it sees that
149
+ rather than whatever `master` was carrying.
150
+
151
+ ## Starting a new minor
152
+
153
+ `master` is the development line of one minor at a time. Moving it from `X.Y` to `X.(Y+1)` is not
154
+ part of any one release — it is the decision that the `X.Y` line is done, taken whenever that
155
+ becomes true — and it is the one moment the version on `master` is changed by hand. Two changes, in
156
+ opposite places:
157
+
158
+ 1. **Branch the line being left behind**, from the last `master` commit that belongs to it:
159
+
160
+ ```console
161
+ $ git switch --create aaa-X.Y.x <that commit>
162
+ $ git push -u origin aaa-X.Y.x
163
+ ```
164
+
165
+ Branch from the commit *before* the bump below, so the branch keeps the version its line was
166
+ released under. Patch releases of `X.Y` are cut from here from now on, with their changes
167
+ cherry-picked from `master` — see [Backports](#backports). The `aaa-` prefix carries no meaning
168
+ beyond sorting the release branches to the top of the branch list.
169
+
170
+ 2. **Bump `master`** to `X.(Y+1).0.dev`, in a pull request labeled `skip-changelog` like the
171
+ release pull request itself.
172
+
173
+ One loose end that is easy to forget: **the release note of the new line**. `rake gem:gh_release`
174
+ links every published release to `https://github.com/soutaro/steep/wiki/Release-Note-X.Y`, built
175
+ from the version number without checking that the page is there. Nothing has to be written when
176
+ the line starts — the page comes together as the first release proper of the line comes into view
177
+ — but it does have to exist by the time that release is published, or its notes link to an empty
178
+ page.
179
+
180
+ ## Backports
181
+
182
+ A patch release is cut from a release branch (`aaa-X.Y.x`), and what it carries beyond the previous
183
+ release is cherry-picked from the development line. Cherry-pick with `-x`:
184
+
185
+ ```console
186
+ $ git cherry-pick -x <commit>
187
+ ```
188
+
189
+ `-x` records the commit the change was copied from, and that recorded line is what `rake
190
+ gem:changelog` follows to reach the pull request the change was written and reviewed in. Without
191
+ it, the only pull request a backported commit is associated with is the one that carried the
192
+ backport, which says nothing about the change and is the same for every commit it brought over.
193
+
194
+ ## Assembling the changelog without `gh`
195
+
196
+ A release can be prepared from a Claude Code on the web session, with one exception:
197
+ `gem:changelog` and `gem:changelog:json` cannot run there. Both go through `gh`, and in such a
198
+ session `api.github.com` is blocked at the agent proxy for anything the shell does. `gh` is not
199
+ installed, installing it does not help, and rewriting the tasks against REST or Net::HTTP would be
200
+ blocked the same way — the refusal is keyed on the session rather than on the client.
201
+
202
+ Nothing else in the release is affected. `gem:check_release` and `gem:tag` read git and the working
203
+ tree, and `gem:gh_release` runs on a runner, where `gh` and `github.token` both work.
204
+
205
+ What the session does have is the GitHub MCP server, which reaches the API through its own
206
+ credentials. The changelog is assembled with its tools, in the three steps the rake task takes.
207
+
208
+ **1. Where the changelog starts.** The rule is `changelog_base`: a prerelease starts from the
209
+ latest tag, a release proper skips the prerelease tags. Tags are not fetched by default.
210
+
211
+ ```console
212
+ $ git fetch origin --tags
213
+ $ git describe --tags --match 'v*' --abbrev=0 --exclude 'v*.pre*' --exclude 'v*.dev*'
214
+ v2.0.0
215
+ ```
216
+
217
+ Drop the two `--exclude` flags for a prerelease, which starts at the latest tag whatever it is.
218
+
219
+ **2. The commits.**
220
+
221
+ ```console
222
+ $ git log --format=%H v2.0.0..HEAD
223
+ ```
224
+
225
+ **3. The pull requests they came from.** List the merged pull requests with `list_pull_requests`
226
+ (`base: master`, `state: closed`, `sort: updated`, `direction: desc`, and `fields: number, title,
227
+ labels, merged_at, head`), paging back until `merged_at` predates the base tag, and keep the ones
228
+ whose `head.sha` appears in the commit list from step 2. That intersection is what the task's
229
+ GraphQL `associatedPullRequests` query answers, reached from the other side.
230
+
231
+ Then drop the pull requests labeled `skip-changelog` and format the rest newest first, which is the
232
+ order of step 2:
233
+
234
+ ```markdown
235
+ * {title} ([#{number}](https://github.com/soutaro/steep/pull/{number}))
236
+ ```
237
+
238
+ Sorting them into sections needs the changed files, which `gem:changelog:json` would have supplied:
239
+ `pull_request_read` with `get_files` per pull request, or `get` for the body.
240
+
241
+ Four things about that matching, the first of which is a trap:
242
+
243
+ - **Do not read the numbers from `Merge pull request #N` commit subjects.** A squashed or rebased
244
+ pull request never writes that subject at all. Matching `head.sha` does not have that failure
245
+ mode.
246
+ - `head.sha` is in the history because this repository merges pull requests with merge commits. A
247
+ squashed or rebased one would need its `merge_commit_sha`, which the listing does not carry.
248
+ - The listing reports `merged: false` for pull requests that are merged — the field is not
249
+ populated by that endpoint. Read `merged_at` instead.
250
+ - On a release branch the commits are cherry-picks, so resolve the `(cherry picked from commit
251
+ <sha>)` trailer first and match the recorded origin, as `changelog_origins` does. Matching the
252
+ cherry-pick itself attributes every backport to the pull request that carried it.
253
+
254
+ ## Notes
255
+
256
+ - Prereleases (`X.Y.Z.pre.N`) are only installed with `gem install steep --pre`; a plain
257
+ `gem install steep` is unaffected.
258
+ - `rake 'gem:check_release[X.Y.Z]'` and `rake gem:tag` are what the workflow runs to check the
259
+ release and to create the tag. Both work locally, which is the fallback if the tag ever has to
260
+ be created by hand.
261
+ - Those two tasks and `rake gem:gh_release` come from the Rakefile of the commit being released,
262
+ not from the branch the workflow was dispatched from. Releasing from a release branch
263
+ (`aaa-X.Y.x`) therefore needs the release tooling on that branch as well; without it the run
264
+ fails on the missing task, before publishing anything.
@@ -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
@@ -14,7 +14,7 @@ module Steep
14
14
  end
15
15
 
16
16
  def hash
17
- self.class.hash
17
+ self.class.hash ^ value.hash
18
18
  end
19
19
 
20
20
  alias eql? ==
@@ -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
@@ -354,7 +354,9 @@ Description:
354
354
  Options:
355
355
  BANNER
356
356
  handle_steepfile_option(opts, command)
357
- opts.on("--refork") { command.refork = true }
357
+ opts.on("--[no-]command-socket", "Accept `steep query`/`steep check` connections on a UNIX socket (default: true)") do |v|
358
+ command.command_socket = v ? true : false
359
+ end
358
360
  handle_jobs_option command.jobs_option, opts
359
361
  handle_logging_options opts
360
362
  end.parse!(argv)
@@ -480,7 +482,6 @@ BANNER
480
482
  opts.on("--typecheck") { command.worker_type = :typecheck }
481
483
  handle_steepfile_option(opts, command)
482
484
  opts.on("--name=NAME") {|name| command.worker_name = name }
483
- opts.on("--delay-shutdown") { command.delay_shutdown = true }
484
485
  opts.on("--max-index=COUNT") {|count| command.max_index = Integer(count) }
485
486
  opts.on("--index=INDEX") {|index| command.index = Integer(index) }
486
487
  end.parse!(argv)
@@ -618,8 +619,9 @@ BANNER
618
619
  The user interface and output format may change without deprecation.
619
620
 
620
621
  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
622
+ hover Get hover information (type, documentation) for a position
623
+ definition Get the definition(s) of a class, type alias, constant, or method name
624
+ diagnostics Get the type check diagnostics the server has computed
623
625
 
624
626
  Options:
625
627
  --help Show this help message
@@ -628,6 +630,7 @@ BANNER
628
630
  steep query hover lib/foo.rb:10:5
629
631
  steep query definition RBS::Location
630
632
  steep query definition RBS::Parser.parse_signature
633
+ steep query diagnostics lib/foo.rb sig/foo.rbs
631
634
  HELP
632
635
  return 0
633
636
  end
@@ -714,9 +717,33 @@ BANNER
714
717
  end
715
718
 
716
719
  Drivers::Query.new(stdout: stdout, stderr: stderr).run_definition(names: argv.dup)
720
+ when "diagnostics"
721
+ OptionParser.new do |opts|
722
+ opts.banner = <<BANNER
723
+ Usage: steep query diagnostics [options] [FILE ...]
724
+
725
+ Description:
726
+ Get the type check diagnostics the server has computed.
727
+ Connects to the running Steep server, reloads files changed on disk, waits for the
728
+ type checking to finish, and prints the diagnostics as JSONL (one JSON object per
729
+ line for each file).
730
+
731
+ When FILEs are given, only the diagnostics of those files are printed.
732
+ `"diagnostics": null` means the server has not type checked the file yet.
733
+
734
+ Note:
735
+ This is an experimental command.
736
+ The user interface and output format may change without deprecation.
737
+
738
+ Options:
739
+ BANNER
740
+ handle_logging_options opts
741
+ end.parse!(argv)
742
+
743
+ Drivers::Query.new(stdout: stdout, stderr: stderr).run_diagnostics(paths: argv.dup)
717
744
  else
718
745
  stderr.puts "Unknown query subcommand: #{subcommand}"
719
- stderr.puts " available subcommands: hover, definition"
746
+ stderr.puts " available subcommands: hover, definition, diagnostics"
720
747
  1
721
748
  end
722
749
  end
@@ -110,7 +110,6 @@ module Steep
110
110
  workers = ::Steep::Server::WorkerProcess.start_typecheck_workers(
111
111
  steepfile: @project.steepfile_path,
112
112
  args: [],
113
- delay_shutdown: true,
114
113
  steep_command: nil,
115
114
  count: job_count
116
115
  )
@@ -127,8 +126,7 @@ module Steep
127
126
  reader: server_reader,
128
127
  writer: server_writer,
129
128
  interaction_worker: interaction_worker,
130
- typecheck_workers: workers,
131
- refork: true
129
+ typecheck_workers: workers
132
130
  )
133
131
  master.typecheck_automatically = false
134
132
 
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:)
@@ -112,7 +112,7 @@ module Steep
112
112
  send = case node.type
113
113
  when :send, :csend
114
114
  node
115
- when :block, :numblock
115
+ when :block, :numblock, :itblock
116
116
  node.children[0]
117
117
  end
118
118
 
@@ -163,7 +163,7 @@ module Steep
163
163
  send = case node.type
164
164
  when :send, :csend
165
165
  node
166
- when :block, :numblock
166
+ when :block, :numblock, :itblock
167
167
  node.children[0]
168
168
  end
169
169
 
@@ -245,7 +245,7 @@ module Steep
245
245
  loc ||= node.loc.operator if node.loc.respond_to?(:operator) # steep:ignore NoMethod
246
246
  loc ||= node.loc.selector if node.loc.respond_to?(:selector) # steep:ignore NoMethod
247
247
  loc
248
- when :block
248
+ when :block, :numblock, :itblock
249
249
  node.children[0].loc.selector
250
250
  end
251
251
  super(node: node, location: loc || node.loc.expression)
@@ -1061,7 +1061,7 @@ module Steep
1061
1061
  def header_line
1062
1062
  header =
1063
1063
  case node&.type
1064
- when :send, :csend, :block, :numblock
1064
+ when :send, :csend, :block, :numblock, :itblock
1065
1065
  "The method is deprecated"
1066
1066
  when :const, :casgn
1067
1067
  "The constant is deprecated"
@@ -357,7 +357,6 @@ module Steep
357
357
  typecheck_workers = Server::WorkerProcess.start_typecheck_workers(
358
358
  steepfile: project.steepfile_path,
359
359
  args: command_line_patterns,
360
- delay_shutdown: true,
361
360
  steep_command: jobs_option.steep_command,
362
361
  count: jobs_option.jobs_count_value
363
362
  )
@@ -104,7 +104,6 @@ module Steep
104
104
  typecheck_workers = Server::WorkerProcess.start_typecheck_workers(
105
105
  steepfile: project.steepfile_path,
106
106
  args: [],
107
- delay_shutdown: true,
108
107
  steep_command: jobs_option.steep_command,
109
108
  count: count
110
109
  )
@@ -131,20 +130,13 @@ module Steep
131
130
  stdin_input.each do |path, content|
132
131
  uri = PathHelper.to_uri(project.absolute_path(path))
133
132
 
134
- master.broadcast_notification(
133
+ # The master records the content, and sends it to the worker that type checks the file
134
+ client_writer.write(
135
135
  {
136
136
  method: "textDocument/didChange",
137
137
  params: {
138
- textDocument: { uri: uri, version: 0 },
139
- contentChanges: [{ text: content }]
140
- }
141
- }
142
- )
143
- master.broadcast_notification(
144
- {
145
- method: "textDocument/didSave",
146
- params: {
147
- textDocument: { uri: uri }
138
+ textDocument: { uri: uri, version: 0 },
139
+ contentChanges: [{ text: content }]
148
140
  }
149
141
  }
150
142
  )
@@ -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
 
@@ -8,7 +8,7 @@ module Steep
8
8
  attr_reader :type_check_queue
9
9
  attr_reader :type_check_thread
10
10
  attr_reader :jobs_option
11
- attr_accessor :refork
11
+ attr_accessor :command_socket
12
12
 
13
13
  include Utils::DriverHelper
14
14
 
@@ -19,7 +19,7 @@ module Steep
19
19
  @write_mutex = Mutex.new
20
20
  @type_check_queue = Queue.new
21
21
  @jobs_option = Utils::JobsOption.new(jobs_count_modifier: -1)
22
- @refork = false
22
+ @command_socket = true
23
23
  end
24
24
 
25
25
  def writer
@@ -45,15 +45,44 @@ module Steep
45
45
  reader: reader,
46
46
  writer: writer,
47
47
  interaction_worker: interaction_worker,
48
- typecheck_workers: typecheck_workers,
49
- refork: refork,
48
+ typecheck_workers: typecheck_workers
50
49
  )
51
50
  master.typecheck_automatically = true
52
51
 
53
- master.start()
52
+ socket = start_command_socket(master)
53
+
54
+ begin
55
+ master.start()
56
+ ensure
57
+ socket&.stop
58
+ end
54
59
 
55
60
  0
56
61
  end
62
+
63
+ # Starts accepting `steep query`/`steep check` connections on the UNIX socket
64
+ #
65
+ # Returns `nil` when the command socket is disabled, is not supported on the platform,
66
+ # or is already served by another process.
67
+ #
68
+ def start_command_socket(master)
69
+ return nil unless command_socket
70
+
71
+ configuration = Daemon::Configuration.new(base_dir: project.base_dir.to_s)
72
+ socket = Server::CommandSocket.new(master: master, configuration: configuration)
73
+
74
+ if socket.start
75
+ stderr.puts "Steep command socket is ready: #{configuration.socket_path}"
76
+ socket
77
+ else
78
+ stderr.puts "Steep command socket is not available: #{configuration.socket_path}"
79
+ nil
80
+ end
81
+ rescue NotImplementedError, StandardError => error
82
+ Steep.logger.error { "Failed to start command socket: #{error.inspect}" }
83
+ stderr.puts "Failed to start Steep command socket: #{error.message}"
84
+ nil
85
+ end
57
86
  end
58
87
  end
59
88
  end