spoom 1.1.16 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/spoom/cli/bump.rb +3 -11
- data/lib/spoom/cli/config.rb +4 -3
- data/lib/spoom/cli/coverage.rb +14 -15
- data/lib/spoom/cli/helper.rb +11 -21
- data/lib/spoom/cli/lsp.rb +4 -2
- data/lib/spoom/cli/run.rb +3 -7
- data/lib/spoom/cli.rb +4 -7
- data/lib/spoom/context/bundle.rb +58 -0
- data/lib/spoom/context/exec.rb +50 -0
- data/lib/spoom/context/file_system.rb +93 -0
- data/lib/spoom/context/git.rb +121 -0
- data/lib/spoom/context/sorbet.rb +136 -0
- data/lib/spoom/context.rb +16 -200
- data/lib/spoom/coverage.rb +20 -35
- data/lib/spoom/sorbet.rb +0 -119
- data/lib/spoom/timeline.rb +5 -8
- data/lib/spoom/version.rb +1 -1
- data/lib/spoom.rb +0 -52
- metadata +8 -4
- data/lib/spoom/git.rb +0 -109
@@ -0,0 +1,136 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Spoom
|
5
|
+
class Context
|
6
|
+
# Sorbet features for a context
|
7
|
+
module Sorbet
|
8
|
+
extend T::Sig
|
9
|
+
extend T::Helpers
|
10
|
+
|
11
|
+
requires_ancestor { Context }
|
12
|
+
|
13
|
+
# Run `bundle exec srb` in this context directory
|
14
|
+
sig { params(arg: String, sorbet_bin: T.nilable(String), capture_err: T::Boolean).returns(ExecResult) }
|
15
|
+
def srb(*arg, sorbet_bin: nil, capture_err: true)
|
16
|
+
res = if sorbet_bin
|
17
|
+
exec("#{sorbet_bin} #{arg.join(" ")}", capture_err: capture_err)
|
18
|
+
else
|
19
|
+
bundle_exec("srb #{arg.join(" ")}", capture_err: capture_err)
|
20
|
+
end
|
21
|
+
|
22
|
+
case res.exit_code
|
23
|
+
when Spoom::Sorbet::KILLED_CODE
|
24
|
+
raise Spoom::Sorbet::Error::Killed.new("Sorbet was killed.", res)
|
25
|
+
when Spoom::Sorbet::SEGFAULT_CODE
|
26
|
+
raise Spoom::Sorbet::Error::Segfault.new("Sorbet segfaulted.", res)
|
27
|
+
end
|
28
|
+
|
29
|
+
res
|
30
|
+
end
|
31
|
+
|
32
|
+
sig { params(arg: String, sorbet_bin: T.nilable(String), capture_err: T::Boolean).returns(ExecResult) }
|
33
|
+
def srb_tc(*arg, sorbet_bin: nil, capture_err: true)
|
34
|
+
arg.prepend("tc") unless sorbet_bin
|
35
|
+
T.unsafe(self).srb(*arg, sorbet_bin: sorbet_bin, capture_err: capture_err)
|
36
|
+
end
|
37
|
+
|
38
|
+
sig do
|
39
|
+
params(
|
40
|
+
arg: String,
|
41
|
+
sorbet_bin: T.nilable(String),
|
42
|
+
capture_err: T::Boolean,
|
43
|
+
).returns(T.nilable(T::Hash[String, Integer]))
|
44
|
+
end
|
45
|
+
def srb_metrics(*arg, sorbet_bin: nil, capture_err: true)
|
46
|
+
metrics_file = "metrics.tmp"
|
47
|
+
|
48
|
+
T.unsafe(self).srb_tc(
|
49
|
+
"--metrics-file",
|
50
|
+
metrics_file,
|
51
|
+
*arg,
|
52
|
+
sorbet_bin: sorbet_bin,
|
53
|
+
capture_err: capture_err,
|
54
|
+
)
|
55
|
+
return nil unless file?(metrics_file)
|
56
|
+
|
57
|
+
metrics_path = absolute_path_to(metrics_file)
|
58
|
+
metrics = Spoom::Sorbet::MetricsParser.parse_file(metrics_path)
|
59
|
+
remove!(metrics_file)
|
60
|
+
metrics
|
61
|
+
end
|
62
|
+
|
63
|
+
# List all files typechecked by Sorbet from its `config`
|
64
|
+
sig { params(with_config: T.nilable(Spoom::Sorbet::Config)).returns(T::Array[String]) }
|
65
|
+
def srb_files(with_config: nil)
|
66
|
+
config = with_config || sorbet_config
|
67
|
+
regs = config.ignore.map { |string| Regexp.new(Regexp.escape(string)) }
|
68
|
+
exts = config.allowed_extensions.empty? ? [".rb", ".rbi"] : config.allowed_extensions
|
69
|
+
glob("**/*{#{exts.join(",")}}").reject do |f|
|
70
|
+
regs.any? { |re| re.match?(f) }
|
71
|
+
end.sort
|
72
|
+
end
|
73
|
+
|
74
|
+
sig { params(arg: String, sorbet_bin: T.nilable(String), capture_err: T::Boolean).returns(T.nilable(String)) }
|
75
|
+
def srb_version(*arg, sorbet_bin: nil, capture_err: true)
|
76
|
+
res = T.unsafe(self).srb_tc("--no-config", "--version", *arg, sorbet_bin: sorbet_bin, capture_err: capture_err)
|
77
|
+
return nil unless res.status
|
78
|
+
|
79
|
+
res.out.split(" ")[2]
|
80
|
+
end
|
81
|
+
|
82
|
+
# Does this context has a `sorbet/config` file?
|
83
|
+
sig { returns(T::Boolean) }
|
84
|
+
def has_sorbet_config?
|
85
|
+
file?(Spoom::Sorbet::CONFIG_PATH)
|
86
|
+
end
|
87
|
+
|
88
|
+
sig { returns(Spoom::Sorbet::Config) }
|
89
|
+
def sorbet_config
|
90
|
+
Spoom::Sorbet::Config.parse_string(read_sorbet_config)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Read the contents of `sorbet/config` in this context directory
|
94
|
+
sig { returns(String) }
|
95
|
+
def read_sorbet_config
|
96
|
+
read(Spoom::Sorbet::CONFIG_PATH)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Set the `contents` of `sorbet/config` in this context directory
|
100
|
+
sig { params(contents: String, append: T::Boolean).void }
|
101
|
+
def write_sorbet_config!(contents, append: false)
|
102
|
+
write!(Spoom::Sorbet::CONFIG_PATH, contents, append: append)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Read the strictness sigil from the file at `relative_path` (returns `nil` if no sigil)
|
106
|
+
sig { params(relative_path: String).returns(T.nilable(String)) }
|
107
|
+
def read_file_strictness(relative_path)
|
108
|
+
Spoom::Sorbet::Sigils.file_strictness(absolute_path_to(relative_path))
|
109
|
+
end
|
110
|
+
|
111
|
+
# Get the commit introducing the `sorbet/config` file
|
112
|
+
sig { returns(T.nilable(Spoom::Git::Commit)) }
|
113
|
+
def sorbet_intro_commit
|
114
|
+
res = git_log("--diff-filter=A --format='%h %at' -1 -- sorbet/config")
|
115
|
+
return nil unless res.status
|
116
|
+
|
117
|
+
out = res.out.strip
|
118
|
+
return nil if out.empty?
|
119
|
+
|
120
|
+
Spoom::Git::Commit.parse_line(out)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Get the commit removing the `sorbet/config` file
|
124
|
+
sig { returns(T.nilable(Spoom::Git::Commit)) }
|
125
|
+
def sorbet_removal_commit
|
126
|
+
res = git_log("--diff-filter=D --format='%h %at' -1 -- sorbet/config")
|
127
|
+
return nil unless res.status
|
128
|
+
|
129
|
+
out = res.out.strip
|
130
|
+
return nil if out.empty?
|
131
|
+
|
132
|
+
Spoom::Git::Commit.parse_line(out)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
data/lib/spoom/context.rb
CHANGED
@@ -3,8 +3,15 @@
|
|
3
3
|
|
4
4
|
require "fileutils"
|
5
5
|
require "open3"
|
6
|
+
require "time"
|
6
7
|
require "tmpdir"
|
7
8
|
|
9
|
+
require_relative "context/bundle"
|
10
|
+
require_relative "context/exec"
|
11
|
+
require_relative "context/file_system"
|
12
|
+
require_relative "context/git"
|
13
|
+
require_relative "context/sorbet"
|
14
|
+
|
8
15
|
module Spoom
|
9
16
|
# An abstraction to a Ruby project context
|
10
17
|
#
|
@@ -13,9 +20,11 @@ module Spoom
|
|
13
20
|
class Context
|
14
21
|
extend T::Sig
|
15
22
|
|
16
|
-
|
17
|
-
|
18
|
-
|
23
|
+
include Bundle
|
24
|
+
include Exec
|
25
|
+
include FileSystem
|
26
|
+
include Git
|
27
|
+
include Sorbet
|
19
28
|
|
20
29
|
class << self
|
21
30
|
extend T::Sig
|
@@ -30,6 +39,10 @@ module Spoom
|
|
30
39
|
end
|
31
40
|
end
|
32
41
|
|
42
|
+
# The absolute path to the directory this context is about
|
43
|
+
sig { returns(String) }
|
44
|
+
attr_reader :absolute_path
|
45
|
+
|
33
46
|
# Create a new context about `absolute_path`
|
34
47
|
#
|
35
48
|
# The directory will not be created if it doesn't exist.
|
@@ -38,202 +51,5 @@ module Spoom
|
|
38
51
|
def initialize(absolute_path)
|
39
52
|
@absolute_path = T.let(::File.expand_path(absolute_path), String)
|
40
53
|
end
|
41
|
-
|
42
|
-
# Returns the absolute path to `relative_path` in the context's directory
|
43
|
-
sig { params(relative_path: String).returns(String) }
|
44
|
-
def absolute_path_to(relative_path)
|
45
|
-
File.join(@absolute_path, relative_path)
|
46
|
-
end
|
47
|
-
|
48
|
-
# File System
|
49
|
-
|
50
|
-
# Does the context directory at `absolute_path` exist and is a directory?
|
51
|
-
sig { returns(T::Boolean) }
|
52
|
-
def exist?
|
53
|
-
File.directory?(@absolute_path)
|
54
|
-
end
|
55
|
-
|
56
|
-
# Create the context directory at `absolute_path`
|
57
|
-
sig { void }
|
58
|
-
def mkdir!
|
59
|
-
FileUtils.rm_rf(@absolute_path)
|
60
|
-
FileUtils.mkdir_p(@absolute_path)
|
61
|
-
end
|
62
|
-
|
63
|
-
# List all files in this context matching `pattern`
|
64
|
-
sig { params(pattern: String).returns(T::Array[String]) }
|
65
|
-
def glob(pattern = "**/*")
|
66
|
-
Dir.glob(absolute_path_to(pattern)).map do |path|
|
67
|
-
Pathname.new(path).relative_path_from(@absolute_path).to_s
|
68
|
-
end.sort
|
69
|
-
end
|
70
|
-
|
71
|
-
# List all files at the top level of this context directory
|
72
|
-
sig { returns(T::Array[String]) }
|
73
|
-
def list
|
74
|
-
glob("*")
|
75
|
-
end
|
76
|
-
|
77
|
-
# Does `relative_path` point to an existing file in this context directory?
|
78
|
-
sig { params(relative_path: String).returns(T::Boolean) }
|
79
|
-
def file?(relative_path)
|
80
|
-
File.file?(absolute_path_to(relative_path))
|
81
|
-
end
|
82
|
-
|
83
|
-
# Return the contents of the file at `relative_path` in this context directory
|
84
|
-
#
|
85
|
-
# Will raise if the file doesn't exist.
|
86
|
-
sig { params(relative_path: String).returns(String) }
|
87
|
-
def read(relative_path)
|
88
|
-
File.read(absolute_path_to(relative_path))
|
89
|
-
end
|
90
|
-
|
91
|
-
# Write `contents` in the file at `relative_path` in this context directory
|
92
|
-
#
|
93
|
-
# Append to the file if `append` is true.
|
94
|
-
sig { params(relative_path: String, contents: String, append: T::Boolean).void }
|
95
|
-
def write!(relative_path, contents = "", append: false)
|
96
|
-
absolute_path = absolute_path_to(relative_path)
|
97
|
-
FileUtils.mkdir_p(File.dirname(absolute_path))
|
98
|
-
File.write(absolute_path, contents, mode: append ? "a" : "w")
|
99
|
-
end
|
100
|
-
|
101
|
-
# Remove the path at `relative_path` (recursive + force) in this context directory
|
102
|
-
sig { params(relative_path: String).void }
|
103
|
-
def remove!(relative_path)
|
104
|
-
FileUtils.rm_rf(absolute_path_to(relative_path))
|
105
|
-
end
|
106
|
-
|
107
|
-
# Move the file or directory from `from_relative_path` to `to_relative_path`
|
108
|
-
sig { params(from_relative_path: String, to_relative_path: String).void }
|
109
|
-
def move!(from_relative_path, to_relative_path)
|
110
|
-
destination_path = absolute_path_to(to_relative_path)
|
111
|
-
FileUtils.mkdir_p(File.dirname(destination_path))
|
112
|
-
FileUtils.mv(absolute_path_to(from_relative_path), destination_path)
|
113
|
-
end
|
114
|
-
|
115
|
-
# Delete this context and its content
|
116
|
-
#
|
117
|
-
# Warning: it will `rm -rf` the context directory on the file system.
|
118
|
-
sig { void }
|
119
|
-
def destroy!
|
120
|
-
FileUtils.rm_rf(@absolute_path)
|
121
|
-
end
|
122
|
-
|
123
|
-
# Execution
|
124
|
-
|
125
|
-
# Run a command in this context directory
|
126
|
-
sig { params(command: String, capture_err: T::Boolean).returns(ExecResult) }
|
127
|
-
def exec(command, capture_err: true)
|
128
|
-
Bundler.with_unbundled_env do
|
129
|
-
opts = T.let({ chdir: @absolute_path }, T::Hash[Symbol, T.untyped])
|
130
|
-
|
131
|
-
if capture_err
|
132
|
-
out, err, status = Open3.capture3(command, opts)
|
133
|
-
ExecResult.new(out: out, err: err, status: T.must(status.success?), exit_code: T.must(status.exitstatus))
|
134
|
-
else
|
135
|
-
out, status = Open3.capture2(command, opts)
|
136
|
-
ExecResult.new(out: out, err: nil, status: T.must(status.success?), exit_code: T.must(status.exitstatus))
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
# Bundle
|
142
|
-
|
143
|
-
# Read the `contents` of the Gemfile in this context directory
|
144
|
-
sig { returns(T.nilable(String)) }
|
145
|
-
def read_gemfile
|
146
|
-
read("Gemfile")
|
147
|
-
end
|
148
|
-
|
149
|
-
# Set the `contents` of the Gemfile in this context directory
|
150
|
-
sig { params(contents: String, append: T::Boolean).void }
|
151
|
-
def write_gemfile!(contents, append: false)
|
152
|
-
write!("Gemfile", contents, append: append)
|
153
|
-
end
|
154
|
-
|
155
|
-
# Run a command with `bundle` in this context directory
|
156
|
-
sig { params(command: String, version: T.nilable(String)).returns(ExecResult) }
|
157
|
-
def bundle(command, version: nil)
|
158
|
-
command = "_#{version}_ #{command}" if version
|
159
|
-
exec("bundle #{command}")
|
160
|
-
end
|
161
|
-
|
162
|
-
# Run `bundle install` in this context directory
|
163
|
-
sig { params(version: T.nilable(String)).returns(ExecResult) }
|
164
|
-
def bundle_install!(version: nil)
|
165
|
-
bundle("install", version: version)
|
166
|
-
end
|
167
|
-
|
168
|
-
# Run a command `bundle exec` in this context directory
|
169
|
-
sig { params(command: String, version: T.nilable(String)).returns(ExecResult) }
|
170
|
-
def bundle_exec(command, version: nil)
|
171
|
-
bundle("exec #{command}", version: version)
|
172
|
-
end
|
173
|
-
|
174
|
-
# Git
|
175
|
-
|
176
|
-
# Run a command prefixed by `git` in this context directory
|
177
|
-
sig { params(command: String).returns(ExecResult) }
|
178
|
-
def git(command)
|
179
|
-
exec("git #{command}")
|
180
|
-
end
|
181
|
-
|
182
|
-
# Run `git init` in this context directory
|
183
|
-
#
|
184
|
-
# Warning: passing a branch will run `git init -b <branch>` which is only available in git 2.28+.
|
185
|
-
# In older versions, use `git_init!` followed by `git("checkout -b <branch>")`.
|
186
|
-
sig { params(branch: T.nilable(String)).returns(ExecResult) }
|
187
|
-
def git_init!(branch: nil)
|
188
|
-
if branch
|
189
|
-
git("init -b #{branch}")
|
190
|
-
else
|
191
|
-
git("init")
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
|
-
# Run `git checkout` in this context directory
|
196
|
-
sig { params(ref: String).returns(ExecResult) }
|
197
|
-
def git_checkout!(ref: "main")
|
198
|
-
git("checkout #{ref}")
|
199
|
-
end
|
200
|
-
|
201
|
-
# Get the current git branch in this context directory
|
202
|
-
sig { returns(T.nilable(String)) }
|
203
|
-
def git_current_branch
|
204
|
-
Spoom::Git.current_branch(path: @absolute_path)
|
205
|
-
end
|
206
|
-
|
207
|
-
# Get the last commit in the currently checked out branch
|
208
|
-
sig { params(short_sha: T::Boolean).returns(T.nilable(Git::Commit)) }
|
209
|
-
def git_last_commit(short_sha: true)
|
210
|
-
Spoom::Git.last_commit(path: @absolute_path, short_sha: short_sha)
|
211
|
-
end
|
212
|
-
|
213
|
-
# Sorbet
|
214
|
-
|
215
|
-
# Run `bundle exec srb` in this context directory
|
216
|
-
sig { params(command: String).returns(ExecResult) }
|
217
|
-
def srb(command)
|
218
|
-
bundle_exec("srb #{command}")
|
219
|
-
end
|
220
|
-
|
221
|
-
# Read the contents of `sorbet/config` in this context directory
|
222
|
-
sig { returns(String) }
|
223
|
-
def read_sorbet_config
|
224
|
-
read(Spoom::Sorbet::CONFIG_PATH)
|
225
|
-
end
|
226
|
-
|
227
|
-
# Set the `contents` of `sorbet/config` in this context directory
|
228
|
-
sig { params(contents: String, append: T::Boolean).void }
|
229
|
-
def write_sorbet_config!(contents, append: false)
|
230
|
-
write!(Spoom::Sorbet::CONFIG_PATH, contents, append: append)
|
231
|
-
end
|
232
|
-
|
233
|
-
# Read the strictness sigil from the file at `relative_path` (returns `nil` if no sigil)
|
234
|
-
sig { params(relative_path: String).returns(T.nilable(String)) }
|
235
|
-
def read_file_strictness(relative_path)
|
236
|
-
Spoom::Sorbet::Sigils.file_strictness(absolute_path_to(relative_path))
|
237
|
-
end
|
238
54
|
end
|
239
55
|
end
|
data/lib/spoom/coverage.rb
CHANGED
@@ -12,9 +12,9 @@ module Spoom
|
|
12
12
|
class << self
|
13
13
|
extend T::Sig
|
14
14
|
|
15
|
-
sig { params(
|
16
|
-
def snapshot(
|
17
|
-
config = sorbet_config
|
15
|
+
sig { params(context: Context, rbi: T::Boolean, sorbet_bin: T.nilable(String)).returns(Snapshot) }
|
16
|
+
def snapshot(context, rbi: true, sorbet_bin: nil)
|
17
|
+
config = context.sorbet_config
|
18
18
|
config.allowed_extensions.push(".rb", ".rbi") if config.allowed_extensions.empty?
|
19
19
|
|
20
20
|
new_config = config.copy
|
@@ -27,25 +27,16 @@ module Spoom
|
|
27
27
|
new_config.options_string,
|
28
28
|
]
|
29
29
|
|
30
|
-
metrics =
|
31
|
-
|
32
|
-
path: path,
|
33
|
-
capture_err: true,
|
34
|
-
sorbet_bin: sorbet_bin,
|
35
|
-
)
|
30
|
+
metrics = context.srb_metrics(*flags, sorbet_bin: sorbet_bin)
|
31
|
+
|
36
32
|
# Collect extra information using a different configuration
|
37
33
|
flags << "--ignore sorbet/rbi/"
|
38
|
-
metrics_without_rbis =
|
39
|
-
*flags,
|
40
|
-
path: path,
|
41
|
-
capture_err: true,
|
42
|
-
sorbet_bin: sorbet_bin,
|
43
|
-
)
|
34
|
+
metrics_without_rbis = context.srb_metrics(*flags, sorbet_bin: sorbet_bin)
|
44
35
|
|
45
36
|
snapshot = Snapshot.new
|
46
37
|
return snapshot unless metrics
|
47
38
|
|
48
|
-
last_commit =
|
39
|
+
last_commit = context.git_last_commit
|
49
40
|
snapshot.commit_sha = last_commit&.sha
|
50
41
|
snapshot.commit_timestamp = last_commit&.timestamp
|
51
42
|
|
@@ -79,40 +70,34 @@ module Spoom
|
|
79
70
|
end
|
80
71
|
end
|
81
72
|
|
82
|
-
snapshot.version_static =
|
83
|
-
snapshot.version_runtime =
|
73
|
+
snapshot.version_static = context.gem_version_from_gemfile_lock("sorbet-static")
|
74
|
+
snapshot.version_runtime = context.gem_version_from_gemfile_lock("sorbet-runtime")
|
84
75
|
|
85
|
-
files =
|
76
|
+
files = context.srb_files(with_config: new_config)
|
86
77
|
snapshot.rbi_files = files.count { |file| file.end_with?(".rbi") }
|
87
78
|
|
88
79
|
snapshot
|
89
80
|
end
|
90
81
|
|
91
|
-
sig { params(snapshots: T::Array[Snapshot], palette: D3::ColorPalette
|
92
|
-
def report(snapshots, palette
|
93
|
-
intro_commit =
|
82
|
+
sig { params(context: Context, snapshots: T::Array[Snapshot], palette: D3::ColorPalette).returns(Report) }
|
83
|
+
def report(context, snapshots, palette:)
|
84
|
+
intro_commit = context.sorbet_intro_commit
|
94
85
|
|
95
86
|
Report.new(
|
96
|
-
project_name: File.basename(
|
87
|
+
project_name: File.basename(context.absolute_path),
|
97
88
|
palette: palette,
|
98
89
|
snapshots: snapshots,
|
99
|
-
sigils_tree: sigils_tree(
|
90
|
+
sigils_tree: sigils_tree(context),
|
100
91
|
sorbet_intro_commit: intro_commit&.sha,
|
101
92
|
sorbet_intro_date: intro_commit&.time,
|
102
93
|
)
|
103
94
|
end
|
104
95
|
|
105
|
-
sig { params(
|
106
|
-
def
|
107
|
-
|
108
|
-
end
|
109
|
-
|
110
|
-
sig { params(path: String).returns(FileTree) }
|
111
|
-
def sigils_tree(path: ".")
|
112
|
-
config = sorbet_config(path: path)
|
113
|
-
files = Sorbet.srb_files(config, path: path)
|
96
|
+
sig { params(context: Context).returns(FileTree) }
|
97
|
+
def sigils_tree(context)
|
98
|
+
files = context.srb_files
|
114
99
|
|
115
|
-
extensions =
|
100
|
+
extensions = context.sorbet_config.allowed_extensions
|
116
101
|
extensions = [".rb"] if extensions.empty?
|
117
102
|
extensions -= [".rbi"]
|
118
103
|
|
@@ -120,7 +105,7 @@ module Spoom
|
|
120
105
|
files.select! { |file| file =~ pattern }
|
121
106
|
files.reject! { |file| file =~ %r{/test/} }
|
122
107
|
|
123
|
-
FileTree.new(files, strip_prefix:
|
108
|
+
FileTree.new(files, strip_prefix: context.absolute_path)
|
124
109
|
end
|
125
110
|
end
|
126
111
|
end
|
data/lib/spoom/sorbet.rb
CHANGED
@@ -39,124 +39,5 @@ module Spoom
|
|
39
39
|
|
40
40
|
KILLED_CODE = 137
|
41
41
|
SEGFAULT_CODE = 139
|
42
|
-
|
43
|
-
class << self
|
44
|
-
extend T::Sig
|
45
|
-
|
46
|
-
sig do
|
47
|
-
params(
|
48
|
-
arg: String,
|
49
|
-
path: String,
|
50
|
-
capture_err: T::Boolean,
|
51
|
-
sorbet_bin: T.nilable(String),
|
52
|
-
).returns(ExecResult)
|
53
|
-
end
|
54
|
-
def srb(*arg, path: ".", capture_err: false, sorbet_bin: nil)
|
55
|
-
if sorbet_bin
|
56
|
-
arg.prepend(sorbet_bin)
|
57
|
-
else
|
58
|
-
arg.prepend("bundle", "exec", "srb")
|
59
|
-
end
|
60
|
-
result = Spoom.exec(*T.unsafe(arg), path: path, capture_err: capture_err)
|
61
|
-
|
62
|
-
case result.exit_code
|
63
|
-
when KILLED_CODE
|
64
|
-
raise Error::Killed.new("Sorbet was killed.", result)
|
65
|
-
when SEGFAULT_CODE
|
66
|
-
raise Error::Segfault.new("Sorbet segfaulted.", result)
|
67
|
-
end
|
68
|
-
|
69
|
-
result
|
70
|
-
end
|
71
|
-
|
72
|
-
sig do
|
73
|
-
params(
|
74
|
-
arg: String,
|
75
|
-
path: String,
|
76
|
-
capture_err: T::Boolean,
|
77
|
-
sorbet_bin: T.nilable(String),
|
78
|
-
).returns(ExecResult)
|
79
|
-
end
|
80
|
-
def srb_tc(*arg, path: ".", capture_err: false, sorbet_bin: nil)
|
81
|
-
arg.prepend("tc") unless sorbet_bin
|
82
|
-
srb(*T.unsafe(arg), path: path, capture_err: capture_err, sorbet_bin: sorbet_bin)
|
83
|
-
end
|
84
|
-
|
85
|
-
# List all files typechecked by Sorbet from its `config`
|
86
|
-
sig { params(config: Config, path: String).returns(T::Array[String]) }
|
87
|
-
def srb_files(config, path: ".")
|
88
|
-
regs = config.ignore.map { |string| Regexp.new(Regexp.escape(string)) }
|
89
|
-
exts = config.allowed_extensions.empty? ? [".rb", ".rbi"] : config.allowed_extensions
|
90
|
-
Dir.glob((Pathname.new(path) / "**/*{#{exts.join(",")}}").to_s).reject do |f|
|
91
|
-
regs.any? { |re| re.match?(f) }
|
92
|
-
end.sort
|
93
|
-
end
|
94
|
-
|
95
|
-
sig do
|
96
|
-
params(
|
97
|
-
arg: String,
|
98
|
-
path: String,
|
99
|
-
capture_err: T::Boolean,
|
100
|
-
sorbet_bin: T.nilable(String),
|
101
|
-
).returns(T.nilable(String))
|
102
|
-
end
|
103
|
-
def srb_version(*arg, path: ".", capture_err: false, sorbet_bin: nil)
|
104
|
-
result = T.let(
|
105
|
-
T.unsafe(self).srb_tc(
|
106
|
-
"--no-config",
|
107
|
-
"--version",
|
108
|
-
*arg,
|
109
|
-
path: path,
|
110
|
-
capture_err: capture_err,
|
111
|
-
sorbet_bin: sorbet_bin,
|
112
|
-
),
|
113
|
-
ExecResult,
|
114
|
-
)
|
115
|
-
return nil unless result.status
|
116
|
-
|
117
|
-
result.out.split(" ")[2]
|
118
|
-
end
|
119
|
-
|
120
|
-
sig do
|
121
|
-
params(
|
122
|
-
arg: String,
|
123
|
-
path: String,
|
124
|
-
capture_err: T::Boolean,
|
125
|
-
sorbet_bin: T.nilable(String),
|
126
|
-
).returns(T.nilable(T::Hash[String, Integer]))
|
127
|
-
end
|
128
|
-
def srb_metrics(*arg, path: ".", capture_err: false, sorbet_bin: nil)
|
129
|
-
metrics_file = "metrics.tmp"
|
130
|
-
metrics_path = "#{path}/#{metrics_file}"
|
131
|
-
T.unsafe(self).srb_tc(
|
132
|
-
"--metrics-file",
|
133
|
-
metrics_file,
|
134
|
-
*arg,
|
135
|
-
path: path,
|
136
|
-
capture_err: capture_err,
|
137
|
-
sorbet_bin: sorbet_bin,
|
138
|
-
)
|
139
|
-
if File.exist?(metrics_path)
|
140
|
-
metrics = Spoom::Sorbet::MetricsParser.parse_file(metrics_path)
|
141
|
-
File.delete(metrics_path)
|
142
|
-
return metrics
|
143
|
-
end
|
144
|
-
nil
|
145
|
-
end
|
146
|
-
|
147
|
-
# Get `gem` version from the `Gemfile.lock` content
|
148
|
-
#
|
149
|
-
# Returns `nil` if `gem` cannot be found in the Gemfile.
|
150
|
-
sig { params(gem: String, path: String).returns(T.nilable(String)) }
|
151
|
-
def version_from_gemfile_lock(gem: "sorbet", path: ".")
|
152
|
-
gemfile_path = "#{path}/Gemfile.lock"
|
153
|
-
return nil unless File.exist?(gemfile_path)
|
154
|
-
|
155
|
-
content = File.read(gemfile_path).match(/^ #{gem} \(.*(\d+\.\d+\.\d+).*\)/)
|
156
|
-
return nil unless content
|
157
|
-
|
158
|
-
content[1]
|
159
|
-
end
|
160
|
-
end
|
161
42
|
end
|
162
43
|
end
|
data/lib/spoom/timeline.rb
CHANGED
@@ -1,17 +1,15 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
-
require_relative "git"
|
5
|
-
|
6
4
|
module Spoom
|
7
5
|
class Timeline
|
8
6
|
extend T::Sig
|
9
7
|
|
10
|
-
sig { params(
|
11
|
-
def initialize(from, to
|
8
|
+
sig { params(context: Context, from: Time, to: Time).void }
|
9
|
+
def initialize(context, from, to)
|
10
|
+
@context = context
|
12
11
|
@from = from
|
13
12
|
@to = to
|
14
|
-
@path = path
|
15
13
|
end
|
16
14
|
|
17
15
|
# Return one commit for each month between `from` and `to`
|
@@ -37,17 +35,16 @@ module Spoom
|
|
37
35
|
sig { params(dates: T::Array[Time]).returns(T::Array[Git::Commit]) }
|
38
36
|
def commits_for_dates(dates)
|
39
37
|
dates.map do |t|
|
40
|
-
result =
|
38
|
+
result = @context.git_log(
|
41
39
|
"--since='#{t}'",
|
42
40
|
"--until='#{t.to_date.next_month}'",
|
43
41
|
"--format='format:%h %at'",
|
44
42
|
"--author-date-order",
|
45
43
|
"-1",
|
46
|
-
path: @path,
|
47
44
|
)
|
48
45
|
next if result.out.empty?
|
49
46
|
|
50
|
-
Git.
|
47
|
+
Spoom::Git::Commit.parse_line(result.out.strip)
|
51
48
|
end.compact.uniq(&:sha)
|
52
49
|
end
|
53
50
|
end
|
data/lib/spoom/version.rb
CHANGED