steep 2.0.0 → 2.1.0.dev.1
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/README.md +1 -1
- data/Rakefile +350 -147
- data/doc/release.md +264 -0
- data/lib/steep/diagnostic/ruby.rb +4 -4
- data/lib/steep/node_helper.rb +2 -2
- data/lib/steep/services/goto_service.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 +6 -6
- data/lib/steep/type_construction.rb +17 -3
- data/lib/steep/type_inference/logic_type_interpreter.rb +6 -3
- data/lib/steep/typing.rb +3 -3
- data/lib/steep/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9985b9a834986497b037bbe836f6646aad30fcc1dfa33cf187a3f6aea572d5c6
|
|
4
|
+
data.tar.gz: a3ef68064767f01a511e48b02598f4dffa6c403554898b59a76762a732aaa448
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '018023551867526507d0b4a490a871c7b06e7bce17aedc87a1e6622d21fc5de212530fd34b94b4fc81fe45e618312433c5a4426969a27bd2144a9a84ee40c874'
|
|
7
|
+
data.tar.gz: 5d5c55850108f163260d34f0745e684efa15c922d22a9b417ac41272757c78b579656229ecf191d8e14e33b39cae015cc8fc876d8f4ccdd1fba99045e467a725
|
data/README.md
CHANGED
|
@@ -247,7 +247,7 @@ Use `bundle exec rake -T` to see all available tasks.
|
|
|
247
247
|
|
|
248
248
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
249
249
|
|
|
250
|
-
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
250
|
+
To install this gem onto your local machine, run `bundle exec rake install`. Releases are cut by the `Release gem` workflow rather than from a working copy -- see [doc/release.md](doc/release.md).
|
|
251
251
|
|
|
252
252
|
## Contributing
|
|
253
253
|
|
data/Rakefile
CHANGED
|
@@ -29,210 +29,413 @@ namespace :test do
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
# Pull requests with one of these labels are omitted from the changelog.
|
|
33
|
+
CHANGELOG_SKIP_LABELS = ["skip-changelog"]
|
|
34
|
+
|
|
35
|
+
# The tags a release proper starts *after*, rather than at.
|
|
36
|
+
CHANGELOG_PRERELEASE_TAGS = ["v*.pre*", "v*.dev*"]
|
|
37
|
+
|
|
38
|
+
# Resolves the commit-ish the changelog of the release being prepared starts from.
|
|
39
|
+
#
|
|
40
|
+
# `version` is a version number, a tag, or any commit-ish. When it is omitted, the base follows
|
|
41
|
+
# `Steep::VERSION`:
|
|
42
|
+
#
|
|
43
|
+
# * `X.Y.Z.pre.N` documents what changed since `X.Y.Z.pre.N-1`, so it starts from the latest tag.
|
|
44
|
+
# * `X.Y.Z` documents the whole cycle, the prereleases included, so it skips the prerelease tags
|
|
45
|
+
# in between and starts from the previous release proper.
|
|
46
|
+
#
|
|
47
|
+
# This is the step that is easy to get wrong by hand: on a release proper the latest tag is a
|
|
48
|
+
# prerelease, so the obvious default would produce only the tail of the cycle. Passing a version
|
|
49
|
+
# explicitly overrides all of it.
|
|
50
|
+
#
|
|
51
|
+
def changelog_base(version)
|
|
52
|
+
require "open3"
|
|
37
53
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
54
|
+
from =
|
|
55
|
+
if version
|
|
56
|
+
# `2.1.0` and `v2.1.0` both mean the tag `v2.1.0`, while `master` or a SHA is used as is.
|
|
57
|
+
version.match?(/\A\d/) ? "v#{version}" : version
|
|
58
|
+
else
|
|
59
|
+
command = ["git", "describe", "--tags", "--match", "v*", "--abbrev=0"]
|
|
60
|
+
unless Gem::Version.new(Steep::VERSION).prerelease?
|
|
61
|
+
CHANGELOG_PRERELEASE_TAGS.each { |glob| command.push("--exclude", glob) }
|
|
62
|
+
end
|
|
44
63
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
end
|
|
64
|
+
output, status = Open3.capture2(*command)
|
|
65
|
+
raise "🚨 Cannot detect the tag the changelog starts from. Give the previous version explicitly." unless status.success?
|
|
66
|
+
output.chomp
|
|
67
|
+
end
|
|
50
68
|
|
|
51
|
-
|
|
69
|
+
_, status = Open3.capture2("git", "rev-parse", "--verify", "--quiet", "#{from}^{commit}")
|
|
70
|
+
raise "🚨 No such commit-ish: `#{from}`" unless status.success?
|
|
52
71
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"pr",
|
|
56
|
-
"list",
|
|
57
|
-
"--json",
|
|
58
|
-
"url,title,number",
|
|
59
|
-
"--limit=100",
|
|
60
|
-
"--search" ,
|
|
61
|
-
"milestone:\"#{milestone}\" is:merged sort:updated-desc -label:Released"
|
|
62
|
-
]
|
|
72
|
+
from
|
|
73
|
+
end
|
|
63
74
|
|
|
75
|
+
# Runs a GraphQL query against the repository of the working directory.
|
|
76
|
+
#
|
|
77
|
+
# `body` is the selection set inside `repository`, so a query can use the `$owner` and `$name`
|
|
78
|
+
# variables. Returns the contents of `data.repository`.
|
|
79
|
+
#
|
|
80
|
+
def changelog_graphql(body)
|
|
64
81
|
require "open3"
|
|
65
|
-
output, status = Open3.capture2(*command)
|
|
66
|
-
raise status.inspect unless status.success?
|
|
67
|
-
|
|
68
82
|
require "json"
|
|
69
|
-
json = JSON.parse(output, symbolize_names: true)
|
|
70
83
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
@changelog_repository ||=
|
|
85
|
+
begin
|
|
86
|
+
output, status = Open3.capture2("gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner")
|
|
87
|
+
raise status.inspect unless status.success?
|
|
88
|
+
output.chomp.split("/", 2)
|
|
75
89
|
end
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
90
|
+
owner, name = @changelog_repository
|
|
91
|
+
|
|
92
|
+
query = <<~GRAPHQL
|
|
93
|
+
query($owner: String!, $name: String!) {
|
|
94
|
+
repository(owner: $owner, name: $name) {
|
|
95
|
+
#{body}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
GRAPHQL
|
|
99
|
+
|
|
100
|
+
output, status = Open3.capture2(
|
|
101
|
+
"gh", "api", "graphql",
|
|
102
|
+
"-f", "query=#{query}",
|
|
103
|
+
"-f", "owner=#{owner}",
|
|
104
|
+
"-f", "name=#{name}",
|
|
105
|
+
binmode: true
|
|
106
|
+
)
|
|
107
|
+
raise status.inspect unless status.success?
|
|
108
|
+
|
|
109
|
+
# GitHub always answers in UTF-8, while the default external encoding follows the locale. Without
|
|
110
|
+
# this, a pull request body with an emoji fails to parse under `LANG=C`, as in GitHub Actions.
|
|
111
|
+
JSON.parse(output.force_encoding(Encoding::UTF_8), symbolize_names: true).dig(:data, :repository)
|
|
79
112
|
end
|
|
80
113
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
major, minor, patch, pre = Steep::VERSION.split(".", 4)
|
|
86
|
-
major = major.to_i
|
|
87
|
-
minor = minor.to_i
|
|
88
|
-
patch = patch.to_i
|
|
114
|
+
# Lists the commits between `from` and `HEAD`, newest first.
|
|
115
|
+
#
|
|
116
|
+
def changelog_commits(from)
|
|
117
|
+
require "open3"
|
|
89
118
|
|
|
90
|
-
|
|
119
|
+
output, status = Open3.capture2("git", "log", "--format=%H", "#{from}..HEAD")
|
|
120
|
+
raise status.inspect unless status.success?
|
|
91
121
|
|
|
92
|
-
|
|
122
|
+
output.lines.map(&:chomp).reject(&:empty?)
|
|
123
|
+
end
|
|
93
124
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
125
|
+
# What `git cherry-pick -x` appends to the message of the commit it creates.
|
|
126
|
+
CHERRY_PICK_ORIGIN = /^\(cherry picked from commit ([0-9a-f]{40})\)$/
|
|
127
|
+
|
|
128
|
+
# Maps the commits that record where they were cherry-picked from to that commit.
|
|
129
|
+
#
|
|
130
|
+
# A backport is a cherry-pick, so on a release branch it is the recorded origin, not the commit
|
|
131
|
+
# itself, that leads to the pull request the change was written and reviewed in. Without this a
|
|
132
|
+
# backported change is attributed to the pull request that carried the backport, which says
|
|
133
|
+
# nothing about the change and is the same for every commit it brought over.
|
|
134
|
+
#
|
|
135
|
+
# A commit backported twice -- the development line, then a release branch -- carries one line
|
|
136
|
+
# per hop, appended in order, so the first one is where the change started.
|
|
137
|
+
#
|
|
138
|
+
def changelog_origins(commits)
|
|
139
|
+
return {} if commits.empty?
|
|
97
140
|
|
|
98
|
-
|
|
141
|
+
require "open3"
|
|
99
142
|
|
|
100
|
-
|
|
101
|
-
|
|
143
|
+
# `--no-walk` prints these commits and nothing else. NUL delimiters keep a commit message --
|
|
144
|
+
# which can contain anything, including what this format looks like -- from being read as the
|
|
145
|
+
# format itself.
|
|
146
|
+
output, status = Open3.capture2("git", "log", "--no-walk", "--format=%H%x00%B%x00", *commits, binmode: true)
|
|
147
|
+
raise status.inspect unless status.success?
|
|
102
148
|
|
|
103
|
-
|
|
149
|
+
# Commit messages are UTF-8, while the default external encoding follows the locale. Without
|
|
150
|
+
# this, splitting a message that is not ASCII fails under `LANG=C`, as in GitHub Actions.
|
|
151
|
+
output.force_encoding(Encoding::UTF_8)
|
|
104
152
|
|
|
105
|
-
|
|
106
|
-
|
|
153
|
+
output.split("\0").each_slice(2).each_with_object({}) do |(commit, message), origins|
|
|
154
|
+
commit = commit.to_s.strip
|
|
155
|
+
next if commit.empty?
|
|
107
156
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
157
|
+
origin = message.to_s[CHERRY_PICK_ORIGIN, 1] or next
|
|
158
|
+
origins[commit] = origin
|
|
159
|
+
end
|
|
160
|
+
end
|
|
111
161
|
|
|
112
|
-
|
|
162
|
+
# Asks GitHub which pull requests each commit came from, so that any merge strategy -- merge
|
|
163
|
+
# commit, squash, or rebase -- is handled without parsing commit messages.
|
|
164
|
+
#
|
|
165
|
+
# Returns `{ oid => [pull request, ...] }`, with an empty array for the commits GitHub has no
|
|
166
|
+
# merged pull request for, including the ones it does not know at all.
|
|
167
|
+
#
|
|
168
|
+
def changelog_associated_pull_requests(oids)
|
|
169
|
+
oids.uniq.each_slice(50).each_with_object({}) do |slice, found|
|
|
170
|
+
aliases = slice.map.with_index do |oid, index|
|
|
171
|
+
<<~GRAPHQL
|
|
172
|
+
c#{index}: object(oid: "#{oid}") {
|
|
173
|
+
... on Commit {
|
|
174
|
+
associatedPullRequests(first: 10) {
|
|
175
|
+
nodes {
|
|
176
|
+
number title url merged
|
|
177
|
+
labels(first: 100) { nodes { name } }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
GRAPHQL
|
|
183
|
+
end
|
|
113
184
|
|
|
114
|
-
|
|
115
|
-
gem 'steep', require: false#{pre_requirement}
|
|
116
|
-
```
|
|
185
|
+
response = changelog_graphql(aliases.join("\n"))
|
|
117
186
|
|
|
118
|
-
|
|
187
|
+
slice.each_with_index do |oid, index|
|
|
188
|
+
nodes = response.dig(:"c#{index}", :associatedPullRequests, :nodes) || []
|
|
119
189
|
|
|
120
|
-
|
|
190
|
+
found[oid] = nodes.select { |pr| pr[:merged] }.map do |pr|
|
|
191
|
+
{ number: pr[:number], title: pr[:title], url: pr[:url], labels: pr.dig(:labels, :nodes).map { |label| label[:name] } }
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
121
196
|
|
|
122
|
-
|
|
197
|
+
# Finds the pull requests the commits came from, keeping the order of `commits`.
|
|
198
|
+
#
|
|
199
|
+
# Returns the pull requests for the changelog and the ones omitted by `skip_labels`.
|
|
200
|
+
#
|
|
201
|
+
def changelog_pull_requests(commits, skip_labels: CHANGELOG_SKIP_LABELS)
|
|
202
|
+
origins = changelog_origins(commits)
|
|
203
|
+
found = changelog_associated_pull_requests(commits.map { |commit| origins[commit] || commit })
|
|
204
|
+
|
|
205
|
+
# An origin that leads nowhere -- a commit cherry-picked from a fork, or one that went to the
|
|
206
|
+
# default branch without a pull request -- falls back to the commit in this history, which is
|
|
207
|
+
# at least the backport that brought it here.
|
|
208
|
+
fallbacks = commits.select { |commit| origins[commit] && found.fetch(origins[commit], []).empty? }
|
|
209
|
+
found.update(changelog_associated_pull_requests(fallbacks)) unless fallbacks.empty?
|
|
210
|
+
|
|
211
|
+
pull_requests = {}
|
|
212
|
+
skipped = {}
|
|
213
|
+
|
|
214
|
+
commits.each do |commit|
|
|
215
|
+
prs = found.fetch(origins[commit] || commit, [])
|
|
216
|
+
prs = found.fetch(commit, []) if prs.empty?
|
|
217
|
+
|
|
218
|
+
prs.each do |pr|
|
|
219
|
+
if (pr[:labels] & skip_labels).empty?
|
|
220
|
+
pull_requests[pr[:number]] ||= pr
|
|
221
|
+
else
|
|
222
|
+
skipped[pr[:number]] ||= pr
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
123
226
|
|
|
124
|
-
|
|
227
|
+
[pull_requests.values, skipped.values]
|
|
228
|
+
end
|
|
125
229
|
|
|
126
|
-
|
|
230
|
+
# Fetches the details that help classifying the pull requests: the changed files and the body.
|
|
231
|
+
#
|
|
232
|
+
def changelog_pull_request_details(pull_requests)
|
|
233
|
+
pull_requests.each_slice(50).flat_map do |slice|
|
|
234
|
+
aliases = slice.map do |pr|
|
|
235
|
+
<<~GRAPHQL
|
|
236
|
+
p#{pr[:number]}: pullRequest(number: #{pr[:number]}) {
|
|
237
|
+
body
|
|
238
|
+
author { login }
|
|
239
|
+
files(first: 100) {
|
|
240
|
+
nodes { path }
|
|
241
|
+
pageInfo { hasNextPage }
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
GRAPHQL
|
|
245
|
+
end
|
|
127
246
|
|
|
128
|
-
|
|
247
|
+
details = changelog_graphql(aliases.join("\n"))
|
|
129
248
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
puts
|
|
249
|
+
slice.map do |pr|
|
|
250
|
+
detail = details[:"p#{pr[:number]}"] or next pr
|
|
133
251
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
252
|
+
pr.merge(
|
|
253
|
+
author: detail.dig(:author, :login),
|
|
254
|
+
# The body is a hint for writing the changelog, not a copy source. Keep it short.
|
|
255
|
+
body: detail[:body].to_s.strip.slice(0, 1000),
|
|
256
|
+
files: detail.dig(:files, :nodes).map { |file| file[:path] },
|
|
257
|
+
files_truncated: detail.dig(:files, :pageInfo, :hasNextPage)
|
|
258
|
+
)
|
|
141
259
|
end
|
|
142
|
-
puts
|
|
143
260
|
end
|
|
261
|
+
end
|
|
144
262
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
major = major.to_i
|
|
150
|
-
minor = minor.to_i
|
|
151
|
-
patch = patch.to_i
|
|
263
|
+
# Reports the pull requests omitted by their label, so that they do not disappear silently.
|
|
264
|
+
#
|
|
265
|
+
def warn_skipped_pull_requests(skipped, skip_labels)
|
|
266
|
+
return if skipped.empty?
|
|
152
267
|
|
|
153
|
-
|
|
268
|
+
numbers = skipped.map { |pr| "##{pr[:number]}" }
|
|
269
|
+
numbers = numbers.take(20).push("and #{numbers.size - 20} more") if numbers.size > 20
|
|
154
270
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
271
|
+
$stderr.puts
|
|
272
|
+
$stderr.puts " (⏭️ Skipped #{skipped.size} pull request(s) labeled #{skip_labels.map { |label| "`#{label}`" }.join(" or ")}: #{numbers.join(", ")})"
|
|
273
|
+
end
|
|
158
274
|
|
|
159
|
-
|
|
160
|
-
|
|
275
|
+
# Prints the changelog template listing the pull requests merged between `from` and `HEAD`.
|
|
276
|
+
#
|
|
277
|
+
# The changelog goes to STDOUT and everything else goes to STDERR, so that the output can be
|
|
278
|
+
# piped to another command: `rake gem:changelog | pbcopy`
|
|
279
|
+
#
|
|
280
|
+
def print_changelog(from, skip_labels: CHANGELOG_SKIP_LABELS)
|
|
281
|
+
$stderr.puts "🔍 Finding pull requests merged between `#{from}` and `HEAD`..."
|
|
282
|
+
|
|
283
|
+
commits = changelog_commits(from)
|
|
284
|
+
if commits.empty?
|
|
285
|
+
$stderr.puts " (🤔 There is no commit after `#{from}`.)"
|
|
286
|
+
return
|
|
287
|
+
end
|
|
161
288
|
|
|
162
|
-
|
|
163
|
-
NOTES
|
|
289
|
+
pull_requests, skipped = changelog_pull_requests(commits, skip_labels: skip_labels)
|
|
164
290
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
"
|
|
171
|
-
"--title=#{Steep::VERSION}",
|
|
172
|
-
"--notes=#{notes}"
|
|
173
|
-
]
|
|
174
|
-
|
|
175
|
-
if version.prerelease?
|
|
176
|
-
command << "--prerelease"
|
|
291
|
+
if pull_requests.empty?
|
|
292
|
+
$stderr.puts " (🤔 No pull request is associated to the commits after `#{from}`.)"
|
|
293
|
+
else
|
|
294
|
+
$stderr.puts
|
|
295
|
+
pull_requests.each do |pr|
|
|
296
|
+
puts "* #{pr[:title]} ([##{pr[:number]}](#{pr[:url]}))"
|
|
177
297
|
end
|
|
298
|
+
$stdout.flush
|
|
299
|
+
end
|
|
178
300
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
301
|
+
warn_skipped_pull_requests(skipped, skip_labels)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Prints the same pull requests as `print_changelog` as JSON, with the details that help
|
|
305
|
+
# classifying them into the sections of CHANGELOG.md.
|
|
306
|
+
#
|
|
307
|
+
# This is the input for the release automation, so it always prints a valid JSON document.
|
|
308
|
+
#
|
|
309
|
+
def print_changelog_json(from, skip_labels: CHANGELOG_SKIP_LABELS)
|
|
310
|
+
require "json"
|
|
311
|
+
|
|
312
|
+
$stderr.puts "🔍 Finding pull requests merged between `#{from}` and `HEAD`..."
|
|
313
|
+
|
|
314
|
+
commits = changelog_commits(from)
|
|
315
|
+
pull_requests, skipped = changelog_pull_requests(commits, skip_labels: skip_labels)
|
|
316
|
+
pull_requests = changelog_pull_request_details(pull_requests)
|
|
317
|
+
|
|
318
|
+
$stderr.puts " (📋 #{pull_requests.size} pull request(s))"
|
|
319
|
+
|
|
320
|
+
puts JSON.pretty_generate(
|
|
321
|
+
{
|
|
322
|
+
from: from,
|
|
323
|
+
to: "HEAD",
|
|
324
|
+
pull_requests: pull_requests,
|
|
325
|
+
skipped: skipped
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
$stdout.flush
|
|
329
|
+
|
|
330
|
+
warn_skipped_pull_requests(skipped, skip_labels)
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
namespace :gem do
|
|
334
|
+
desc "Generate changelog template from GH pull requests merged since the previous release"
|
|
335
|
+
task :changelog, [:version] do |_task, args|
|
|
336
|
+
print_changelog(changelog_base(args[:version]))
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
namespace :changelog do
|
|
340
|
+
desc "Print the pull requests of `gem:changelog` as JSON, with the changed files and body of each"
|
|
341
|
+
task :json, [:version] do |_task, args|
|
|
342
|
+
print_changelog_json(changelog_base(args[:version]))
|
|
184
343
|
end
|
|
185
344
|
end
|
|
186
345
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
346
|
+
# There are three kinds of release: `X.Y.Z`, `X.Y.Z.pre.N`, and `X.Y.Z.dev.N`. The
|
|
347
|
+
# `.dev.N` ones are cut from the development line for people who need a specific
|
|
348
|
+
# change early; they are not written up in the changelog, so there are no notes to
|
|
349
|
+
# publish and nothing worth announcing.
|
|
350
|
+
def dev_release?(version)
|
|
351
|
+
Gem::Version.new(version).segments.include?("dev")
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# The body of the topmost section of CHANGELOG.md, which is the release being
|
|
355
|
+
# prepared, minus its own heading.
|
|
356
|
+
#
|
|
357
|
+
# The encoding is explicit because the default external encoding follows the
|
|
358
|
+
# locale, and the changelog is not ASCII.
|
|
359
|
+
#
|
|
360
|
+
def changelog_section(version)
|
|
361
|
+
content = File.read(File.join(__dir__, "CHANGELOG.md"), encoding: Encoding::UTF_8)
|
|
362
|
+
section = content.scan(/^## \d.*?(?=^## \d)/m)[0] or raise "🚨 Cannot find a release section in CHANGELOG.md"
|
|
363
|
+
heading, _, body = section.partition("\n")
|
|
364
|
+
heading.include?(version) or raise "🚨 CHANGELOG.md starts with `#{heading.strip}`, which is not #{version}"
|
|
365
|
+
body.strip
|
|
366
|
+
end
|
|
193
367
|
|
|
194
|
-
|
|
195
|
-
|
|
368
|
+
desc "Check that the working tree is ready to be released as the given version"
|
|
369
|
+
task :check_release, [:version] do |_task, args|
|
|
370
|
+
version = args[:version] or raise "🚨 Pass the version being released: `rake 'gem:check_release[2.1.0]'`"
|
|
371
|
+
Gem::Version.correct?(version) or raise "🚨 `#{version}` is not a version number."
|
|
372
|
+
|
|
373
|
+
# The version being released and the version the commit declares are stated
|
|
374
|
+
# separately -- one by whoever starts the release, one by the commit itself --
|
|
375
|
+
# so that releasing the wrong commit, or releasing the right one under the wrong
|
|
376
|
+
# name, fails here rather than on RubyGems.
|
|
377
|
+
version == Steep::VERSION or
|
|
378
|
+
raise "🚨 Releasing #{version}, but this commit declares `Steep::VERSION = #{Steep::VERSION.inspect}`."
|
|
379
|
+
|
|
380
|
+
if dev_release?(version)
|
|
381
|
+
puts "✅ #{version} is the version of this commit. It is a dev release, so CHANGELOG.md is not checked."
|
|
196
382
|
else
|
|
197
|
-
|
|
383
|
+
changelog_section(version)
|
|
384
|
+
puts "✅ #{version} is the version of this commit, and CHANGELOG.md documents it."
|
|
198
385
|
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
desc "Create and push the `vX.Y.Z` tag for Steep::VERSION"
|
|
389
|
+
task :tag do
|
|
390
|
+
tag = "v#{Steep::VERSION}"
|
|
391
|
+
|
|
392
|
+
# Annotated, so that the tag carries its own author and date rather than
|
|
393
|
+
# borrowing the tagged commit's.
|
|
394
|
+
sh "git", "tag", "--annotate", "--message", "Steep #{Steep::VERSION}", tag
|
|
395
|
+
sh "git", "push", "origin", tag
|
|
396
|
+
|
|
397
|
+
puts "🏷️ Pushed #{tag}."
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
desc "Publish the GitHub release for Steep::VERSION, unless it is a `.dev.` version"
|
|
401
|
+
task :gh_release do
|
|
402
|
+
require "open3"
|
|
403
|
+
|
|
404
|
+
version = Gem::Version.new(Steep::VERSION)
|
|
405
|
+
major, minor, *_ = Steep::VERSION.split(".")
|
|
406
|
+
tag = "v#{Steep::VERSION}"
|
|
199
407
|
|
|
200
|
-
if
|
|
201
|
-
puts "
|
|
408
|
+
if dev_release?(Steep::VERSION)
|
|
409
|
+
puts "⏭️ #{Steep::VERSION} is a dev release, so there is no GitHub release to publish."
|
|
202
410
|
next
|
|
203
411
|
end
|
|
204
412
|
|
|
205
|
-
|
|
413
|
+
# The release is created against an existing tag, so that the artifacts and the
|
|
414
|
+
# notes describe a commit that is already immutable.
|
|
415
|
+
_, status = Open3.capture2("git", "rev-parse", "--verify", "--quiet", "#{tag}^{commit}")
|
|
416
|
+
raise "🚨 No such tag: `#{tag}`. Tag the release before creating the GitHub release." unless status.success?
|
|
206
417
|
|
|
418
|
+
notes = <<~NOTES
|
|
419
|
+
[Release note](https://github.com/soutaro/steep/wiki/Release-Note-#{major}.#{minor})
|
|
420
|
+
|
|
421
|
+
#{changelog_section(Steep::VERSION)}
|
|
422
|
+
NOTES
|
|
423
|
+
|
|
424
|
+
# Published rather than drafted: the notes are the changelog section that was
|
|
425
|
+
# already reviewed in the release pull request, so there is nothing left to edit.
|
|
207
426
|
command = [
|
|
208
|
-
"gh",
|
|
209
|
-
"
|
|
210
|
-
"
|
|
211
|
-
"--json",
|
|
212
|
-
"url,title,number",
|
|
213
|
-
"--search" ,
|
|
214
|
-
"milestone:\"#{milestone}\" is:merged sort:updated-desc -label:Released"
|
|
427
|
+
"gh", "release", "create", tag,
|
|
428
|
+
"--title=#{Steep::VERSION}",
|
|
429
|
+
"--notes=#{notes}"
|
|
215
430
|
]
|
|
431
|
+
command << "--prerelease" if version.prerelease?
|
|
216
432
|
|
|
217
|
-
require "open3"
|
|
218
433
|
output, status = Open3.capture2(*command)
|
|
219
|
-
raise status.inspect unless status.success?
|
|
434
|
+
raise "🚨 `gh release create` failed: #{status.inspect}" unless status.success?
|
|
220
435
|
|
|
221
|
-
|
|
222
|
-
json = JSON.parse(output, symbolize_names: true)
|
|
223
|
-
|
|
224
|
-
puts " ✅ Found #{json.size} PRs..."
|
|
225
|
-
|
|
226
|
-
json.each do |pr|
|
|
227
|
-
puts "🧐 Updating #{pr[:url]}..."
|
|
228
|
-
output, status = Open3.capture2("gh", "pr", "edit", pr[:number].to_s, "--add-label", "Released")
|
|
229
|
-
raise status.inspect unless status.success?
|
|
230
|
-
puts " ✅ Done!"
|
|
231
|
-
sleep 0.5
|
|
232
|
-
end
|
|
436
|
+
puts "📝 Released #{tag}: #{output.chomp}"
|
|
233
437
|
end
|
|
234
438
|
end
|
|
235
|
-
|
|
236
439
|
namespace :rbs do
|
|
237
440
|
task :watch do
|
|
238
441
|
require "listen"
|
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.
|
|
@@ -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"
|
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
|
|
@@ -422,7 +422,7 @@ module Steep
|
|
|
422
422
|
when :send
|
|
423
423
|
location = (_ = node.location) #: Parser::AST::_SelectorLocation
|
|
424
424
|
if test_ast_location(location.selector, line: line, column: column)
|
|
425
|
-
if (parent = parents[0]) && parent.type == :block && parent.children[0] === node
|
|
425
|
+
if (parent = parents[0]) && (parent.type == :block || parent.type == :numblock || parent.type == :itblock) && parent.children[0] === node
|
|
426
426
|
node = parents[0]
|
|
427
427
|
end
|
|
428
428
|
|
|
@@ -59,7 +59,7 @@ module Steep
|
|
|
59
59
|
if begin_loc.end_pos <= pos && pos <= end_loc.begin_pos
|
|
60
60
|
# Given position is between open/close parens of args of send node
|
|
61
61
|
|
|
62
|
-
if parent && (parent.type == :block || parent.type == :numblock) && node.equal?(parent.children[0])
|
|
62
|
+
if parent && (parent.type == :block || parent.type == :numblock || parent.type == :itblock) && node.equal?(parent.children[0])
|
|
63
63
|
send_node = parent
|
|
64
64
|
else
|
|
65
65
|
send_node = node
|
data/lib/steep/source.rb
CHANGED
|
@@ -31,7 +31,7 @@ module Steep
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def self.new_parser
|
|
34
|
-
Prism::Translation::
|
|
34
|
+
Prism::Translation::Parser34.new(Builder.new).tap do |parser|
|
|
35
35
|
parser.diagnostics.all_errors_are_fatal = true
|
|
36
36
|
parser.diagnostics.ignore_warnings = true
|
|
37
37
|
end
|
|
@@ -257,7 +257,7 @@ module Steep
|
|
|
257
257
|
annot.line or next
|
|
258
258
|
|
|
259
259
|
case node.type
|
|
260
|
-
when :def, :module, :class, :block, :numblock, :ensure, :defs, :resbody
|
|
260
|
+
when :def, :module, :class, :block, :numblock, :itblock, :ensure, :defs, :resbody
|
|
261
261
|
location = node.loc
|
|
262
262
|
location.line <= annot.line && annot.line < location.last_line
|
|
263
263
|
else
|
|
@@ -565,13 +565,13 @@ module Steep
|
|
|
565
565
|
end
|
|
566
566
|
]
|
|
567
567
|
)
|
|
568
|
-
when :numblock
|
|
569
|
-
send,
|
|
568
|
+
when :numblock, :itblock
|
|
569
|
+
send, arg, body = node.children
|
|
570
570
|
node = node.updated(
|
|
571
571
|
nil,
|
|
572
572
|
[
|
|
573
573
|
map_child_node(send) {|child| insert_type_node(child, child_assertions) },
|
|
574
|
-
|
|
574
|
+
arg,
|
|
575
575
|
insert_type_node(body, child_assertions)
|
|
576
576
|
]
|
|
577
577
|
)
|
|
@@ -648,7 +648,7 @@ module Steep
|
|
|
648
648
|
case node.type
|
|
649
649
|
when :send, :csend
|
|
650
650
|
node
|
|
651
|
-
when :block, :numblock
|
|
651
|
+
when :block, :numblock, :itblock
|
|
652
652
|
send = node.children[0]
|
|
653
653
|
case send.type
|
|
654
654
|
when :send, :csend
|
|
@@ -2696,7 +2696,7 @@ module Steep
|
|
|
2696
2696
|
constr.add_typing(node, type: type)
|
|
2697
2697
|
end
|
|
2698
2698
|
|
|
2699
|
-
when :block, :numblock, :send, :csend
|
|
2699
|
+
when :block, :numblock, :itblock, :send, :csend
|
|
2700
2700
|
synthesize_sendish(node, hint: hint, tapp: nil)
|
|
2701
2701
|
|
|
2702
2702
|
when :forwarded_args, :forward_arg
|
|
@@ -2772,6 +2772,20 @@ module Steep
|
|
|
2772
2772
|
|
|
2773
2773
|
params = Parser::AST::Node.new(:args, arg_nodes)
|
|
2774
2774
|
|
|
2775
|
+
if send_node.type == :lambda
|
|
2776
|
+
# @type var node: Parser::AST::Node & Parser::AST::_BlockNode
|
|
2777
|
+
type_lambda(node, params_node: params, body_node: body, type_hint: hint)
|
|
2778
|
+
else
|
|
2779
|
+
type_send(node, send_node: send_node, block_params: params, block_body: body, unwrap: send_node.type == :csend, tapp: tapp, hint: hint)
|
|
2780
|
+
end
|
|
2781
|
+
end
|
|
2782
|
+
when :itblock
|
|
2783
|
+
yield_self do
|
|
2784
|
+
send_node, _name, body = node.children
|
|
2785
|
+
|
|
2786
|
+
arg_nodes = [Parser::AST::Node.new(:procarg0, [:it])]
|
|
2787
|
+
params = Parser::AST::Node.new(:args, arg_nodes)
|
|
2788
|
+
|
|
2775
2789
|
if send_node.type == :lambda
|
|
2776
2790
|
# @type var node: Parser::AST::Node & Parser::AST::_BlockNode
|
|
2777
2791
|
type_lambda(node, params_node: params, body_node: body, type_hint: hint)
|
|
@@ -3346,7 +3360,7 @@ module Steep
|
|
|
3346
3360
|
end
|
|
3347
3361
|
end
|
|
3348
3362
|
|
|
3349
|
-
if node.type == :csend || ((node.type == :block || node.type == :numblock) && node.children[0].type == :csend)
|
|
3363
|
+
if node.type == :csend || ((node.type == :block || node.type == :numblock || node.type == :itblock) && node.children[0].type == :csend)
|
|
3350
3364
|
optional_type = AST::Types::Union.build(types: [call.return_type, AST::Builtin.nil_type])
|
|
3351
3365
|
call = call.with_return_type(optional_type)
|
|
3352
3366
|
end
|
|
@@ -3509,7 +3523,7 @@ module Steep
|
|
|
3509
3523
|
|
|
3510
3524
|
when AST::Types::Any
|
|
3511
3525
|
case node.type
|
|
3512
|
-
when :block, :numblock
|
|
3526
|
+
when :block, :numblock, :itblock
|
|
3513
3527
|
# @type var node: Parser::AST::Node & Parser::AST::_BlockNode
|
|
3514
3528
|
block_annotations = source.annotations(block: node, factory: checker.factory, context: nesting)
|
|
3515
3529
|
block_params or raise
|
|
@@ -564,9 +564,12 @@ module Steep
|
|
|
564
564
|
false_types << type
|
|
565
565
|
end
|
|
566
566
|
else
|
|
567
|
-
# For
|
|
568
|
-
#
|
|
569
|
-
|
|
567
|
+
# For `==`, narrow only when the literal can be a value of `type`.
|
|
568
|
+
# e.g. `Symbol == :fatal` can be true, but `SomeClass == :fatal` cannot.
|
|
569
|
+
literal_type = AST::Types::Literal.new(value: value_node.children[0])
|
|
570
|
+
if !for_receiver || subtyping?(sub_type: literal_type, super_type: type)
|
|
571
|
+
true_types << literal_type
|
|
572
|
+
end
|
|
570
573
|
false_types << type
|
|
571
574
|
end
|
|
572
575
|
end
|
data/lib/steep/typing.rb
CHANGED
|
@@ -106,7 +106,7 @@ module Steep
|
|
|
106
106
|
set(body_begin_pos..body_end_pos, context)
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
when :block, :numblock
|
|
109
|
+
when :block, :numblock, :itblock
|
|
110
110
|
range = block_range(node)
|
|
111
111
|
set(range, context)
|
|
112
112
|
|
|
@@ -132,7 +132,7 @@ module Steep
|
|
|
132
132
|
node.loc.begin.end_pos # steep:ignore NoMethod
|
|
133
133
|
end
|
|
134
134
|
end_pos = node.loc.end.begin_pos # steep:ignore NoMethod
|
|
135
|
-
when :numblock
|
|
135
|
+
when :numblock, :itblock
|
|
136
136
|
send_node, _ = node.children
|
|
137
137
|
begin_pos = node.loc.begin.end_pos # steep:ignore NoMethod
|
|
138
138
|
end_pos = node.loc.end.begin_pos # steep:ignore NoMethod
|
|
@@ -245,7 +245,7 @@ module Steep
|
|
|
245
245
|
node.loc.begin.end_pos # steep:ignore NoMethod
|
|
246
246
|
end
|
|
247
247
|
end_pos = node.loc.end.begin_pos # steep:ignore NoMethod
|
|
248
|
-
when :numblock
|
|
248
|
+
when :numblock, :itblock
|
|
249
249
|
send_node, _ = node.children
|
|
250
250
|
begin_pos = node.loc.begin.end_pos # steep:ignore NoMethod
|
|
251
251
|
end_pos = node.loc.end.begin_pos # steep:ignore NoMethod
|
data/lib/steep/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: steep
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.1.0.dev.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Soutaro Matsumoto
|
|
@@ -267,6 +267,7 @@ files:
|
|
|
267
267
|
- bin/steep-check.rb
|
|
268
268
|
- bin/steep-prof
|
|
269
269
|
- doc/narrowing.md
|
|
270
|
+
- doc/release.md
|
|
270
271
|
- doc/shape.md
|
|
271
272
|
- exe/steep
|
|
272
273
|
- guides/README.md
|
|
@@ -448,7 +449,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
448
449
|
- !ruby/object:Gem::Version
|
|
449
450
|
version: '0'
|
|
450
451
|
requirements: []
|
|
451
|
-
rubygems_version: 4.0.
|
|
452
|
+
rubygems_version: 4.0.18
|
|
452
453
|
specification_version: 4
|
|
453
454
|
summary: Gradual Typing for Ruby
|
|
454
455
|
test_files: []
|