spoom 1.0.4 → 1.0.5
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 +253 -1
- data/Rakefile +1 -0
- data/lib/spoom.rb +2 -1
- data/lib/spoom/cli.rb +42 -7
- data/lib/spoom/cli/bump.rb +59 -0
- data/lib/spoom/cli/config.rb +51 -0
- data/lib/spoom/cli/coverage.rb +191 -0
- data/lib/spoom/cli/helper.rb +70 -0
- data/lib/spoom/cli/lsp.rb +165 -0
- data/lib/spoom/cli/run.rb +79 -0
- data/lib/spoom/config.rb +1 -1
- data/lib/spoom/coverage.rb +73 -0
- data/lib/spoom/coverage/d3.rb +110 -0
- data/lib/spoom/coverage/d3/base.rb +50 -0
- data/lib/spoom/coverage/d3/circle_map.rb +195 -0
- data/lib/spoom/coverage/d3/pie.rb +175 -0
- data/lib/spoom/coverage/d3/timeline.rb +486 -0
- data/lib/spoom/coverage/report.rb +308 -0
- data/lib/spoom/coverage/snapshot.rb +132 -0
- data/lib/spoom/file_tree.rb +196 -0
- data/lib/spoom/git.rb +98 -0
- data/lib/spoom/printer.rb +81 -0
- data/lib/spoom/sorbet.rb +15 -2
- data/lib/spoom/sorbet/errors.rb +25 -15
- data/lib/spoom/sorbet/lsp.rb +4 -2
- data/lib/spoom/sorbet/lsp/structures.rb +108 -14
- data/lib/spoom/sorbet/metrics.rb +10 -79
- data/lib/spoom/sorbet/sigils.rb +98 -0
- data/lib/spoom/test_helpers/project.rb +103 -0
- data/lib/spoom/timeline.rb +53 -0
- data/lib/spoom/version.rb +1 -1
- metadata +25 -10
- data/lib/spoom/cli/commands/base.rb +0 -36
- data/lib/spoom/cli/commands/config.rb +0 -67
- data/lib/spoom/cli/commands/lsp.rb +0 -156
- data/lib/spoom/cli/commands/run.rb +0 -92
- data/lib/spoom/cli/symbol_printer.rb +0 -71
data/lib/spoom/sorbet/metrics.rb
CHANGED
@@ -1,102 +1,33 @@
|
|
1
1
|
# typed: strict
|
2
2
|
# frozen_string_literal: true
|
3
3
|
|
4
|
+
require_relative "sigils"
|
5
|
+
|
4
6
|
module Spoom
|
5
7
|
module Sorbet
|
6
|
-
|
8
|
+
module MetricsParser
|
7
9
|
extend T::Sig
|
8
10
|
|
9
11
|
DEFAULT_PREFIX = "ruby_typer.unknown.."
|
10
|
-
SIGILS = T.let(["ignore", "false", "true", "strict", "strong", "__STDLIB_INTERNAL"], T::Array[String])
|
11
|
-
|
12
|
-
const :repo, String
|
13
|
-
const :sha, String
|
14
|
-
const :status, String
|
15
|
-
const :branch, String
|
16
|
-
const :timestamp, Integer
|
17
|
-
const :uuid, String
|
18
|
-
const :metrics, T::Hash[String, T.nilable(Integer)]
|
19
12
|
|
20
|
-
sig { params(path: String, prefix: String).returns(
|
13
|
+
sig { params(path: String, prefix: String).returns(T::Hash[String, Integer]) }
|
21
14
|
def self.parse_file(path, prefix = DEFAULT_PREFIX)
|
22
15
|
parse_string(File.read(path), prefix)
|
23
16
|
end
|
24
17
|
|
25
|
-
sig { params(string: String, prefix: String).returns(
|
18
|
+
sig { params(string: String, prefix: String).returns(T::Hash[String, Integer]) }
|
26
19
|
def self.parse_string(string, prefix = DEFAULT_PREFIX)
|
27
20
|
parse_hash(JSON.parse(string), prefix)
|
28
21
|
end
|
29
22
|
|
30
|
-
sig { params(obj: T::Hash[String, T.untyped], prefix: String).returns(
|
23
|
+
sig { params(obj: T::Hash[String, T.untyped], prefix: String).returns(T::Hash[String, Integer]) }
|
31
24
|
def self.parse_hash(obj, prefix = DEFAULT_PREFIX)
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
branch: obj.fetch("branch"),
|
37
|
-
timestamp: obj.fetch("timestamp").to_i,
|
38
|
-
uuid: obj.fetch("uuid"),
|
39
|
-
metrics: obj["metrics"].each_with_object({}) do |metric, all|
|
40
|
-
name = metric["name"]
|
41
|
-
name = name.sub(prefix, '')
|
42
|
-
all[name] = metric["value"].to_i
|
43
|
-
end,
|
44
|
-
)
|
45
|
-
end
|
46
|
-
|
47
|
-
sig { returns(T::Hash[String, T.nilable(Integer)]) }
|
48
|
-
def files_by_strictness
|
49
|
-
SIGILS.each_with_object({}) do |sigil, map|
|
50
|
-
map[sigil] = metrics["types.input.files.sigil.#{sigil}"]
|
25
|
+
obj["metrics"].each_with_object(Hash.new(0)) do |metric, metrics|
|
26
|
+
name = metric["name"]
|
27
|
+
name = name.sub(prefix, '')
|
28
|
+
metrics[name] = metric["value"] || 0
|
51
29
|
end
|
52
30
|
end
|
53
|
-
|
54
|
-
sig { returns(Integer) }
|
55
|
-
def files_count
|
56
|
-
files_by_strictness.values.compact.sum
|
57
|
-
end
|
58
|
-
|
59
|
-
sig { params(key: String).returns(T.nilable(Integer)) }
|
60
|
-
def [](key)
|
61
|
-
metrics[key]
|
62
|
-
end
|
63
|
-
|
64
|
-
sig { returns(String) }
|
65
|
-
def to_s
|
66
|
-
"Metrics<#{repo}-#{timestamp}-#{status}>"
|
67
|
-
end
|
68
|
-
|
69
|
-
sig { params(out: T.any(IO, StringIO)).void }
|
70
|
-
def show(out = $stdout)
|
71
|
-
files = files_count
|
72
|
-
|
73
|
-
out.puts "Sigils:"
|
74
|
-
out.puts " files: #{files}"
|
75
|
-
files_by_strictness.each do |sigil, value|
|
76
|
-
next unless value
|
77
|
-
out.puts " #{sigil}: #{value}#{percent(value, files)}"
|
78
|
-
end
|
79
|
-
|
80
|
-
out.puts "\nMethods:"
|
81
|
-
m = metrics['types.input.methods.total']
|
82
|
-
s = metrics['types.sig.count']
|
83
|
-
out.puts " methods: #{m}"
|
84
|
-
out.puts " signatures: #{s}#{percent(s, m)}"
|
85
|
-
|
86
|
-
out.puts "\nSends:"
|
87
|
-
t = metrics['types.input.sends.typed']
|
88
|
-
s = metrics['types.input.sends.total']
|
89
|
-
out.puts " sends: #{s}"
|
90
|
-
out.puts " typed: #{t}#{percent(t, s)}"
|
91
|
-
end
|
92
|
-
|
93
|
-
private
|
94
|
-
|
95
|
-
sig { params(value: T.nilable(Integer), total: T.nilable(Integer)).returns(String) }
|
96
|
-
def percent(value, total)
|
97
|
-
return "" if value.nil? || total.nil? || total == 0
|
98
|
-
" (#{value * 100 / total}%)"
|
99
|
-
end
|
100
31
|
end
|
101
32
|
end
|
102
33
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# The term "sigil" refers to the magic comment at the top of the file that has the form `# typed: <strictness>`,
|
5
|
+
# where "strictness" represents the level at which Sorbet will report errors
|
6
|
+
# See https://sorbet.org/docs/static for a more complete explanation
|
7
|
+
module Spoom
|
8
|
+
module Sorbet
|
9
|
+
module Sigils
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
STRICTNESS_IGNORE = "ignore"
|
13
|
+
STRICTNESS_FALSE = "false"
|
14
|
+
STRICTNESS_TRUE = "true"
|
15
|
+
STRICTNESS_STRICT = "strict"
|
16
|
+
STRICTNESS_STRONG = "strong"
|
17
|
+
STRICTNESS_INTERNAL = "__STDLIB_INTERNAL"
|
18
|
+
|
19
|
+
VALID_STRICTNESS = T.let([
|
20
|
+
STRICTNESS_IGNORE,
|
21
|
+
STRICTNESS_FALSE,
|
22
|
+
STRICTNESS_TRUE,
|
23
|
+
STRICTNESS_STRICT,
|
24
|
+
STRICTNESS_STRONG,
|
25
|
+
STRICTNESS_INTERNAL,
|
26
|
+
].freeze, T::Array[String])
|
27
|
+
|
28
|
+
SIGIL_REGEXP = T.let(/^#\s*typed\s*:\s*(\w*)\s*$/.freeze, Regexp)
|
29
|
+
|
30
|
+
# returns the full sigil comment string for the passed strictness
|
31
|
+
sig { params(strictness: String).returns(String) }
|
32
|
+
def self.sigil_string(strictness)
|
33
|
+
"# typed: #{strictness}"
|
34
|
+
end
|
35
|
+
|
36
|
+
# returns true if the passed string is a valid strictness (else false)
|
37
|
+
sig { params(strictness: String).returns(T::Boolean) }
|
38
|
+
def self.valid_strictness?(strictness)
|
39
|
+
VALID_STRICTNESS.include?(strictness.strip)
|
40
|
+
end
|
41
|
+
|
42
|
+
# returns the strictness of a sigil in the passed file content string (nil if no sigil)
|
43
|
+
sig { params(content: String).returns(T.nilable(String)) }
|
44
|
+
def self.strictness_in_content(content)
|
45
|
+
SIGIL_REGEXP.match(content)&.[](1)
|
46
|
+
end
|
47
|
+
|
48
|
+
# returns a string which is the passed content but with the sigil updated to a new strictness
|
49
|
+
sig { params(content: String, new_strictness: String).returns(String) }
|
50
|
+
def self.update_sigil(content, new_strictness)
|
51
|
+
content.sub(SIGIL_REGEXP, sigil_string(new_strictness))
|
52
|
+
end
|
53
|
+
|
54
|
+
# returns a string containing the strictness of a sigil in a file at the passed path
|
55
|
+
# * returns nil if no sigil
|
56
|
+
sig { params(path: T.any(String, Pathname)).returns(T.nilable(String)) }
|
57
|
+
def self.file_strictness(path)
|
58
|
+
return nil unless File.exist?(path)
|
59
|
+
content = File.read(path)
|
60
|
+
strictness_in_content(content)
|
61
|
+
end
|
62
|
+
|
63
|
+
# changes the sigil in the file at the passed path to the specified new strictness
|
64
|
+
sig { params(path: T.any(String, Pathname), new_strictness: String).returns(T::Boolean) }
|
65
|
+
def self.change_sigil_in_file(path, new_strictness)
|
66
|
+
content = File.read(path)
|
67
|
+
new_content = update_sigil(content, new_strictness)
|
68
|
+
|
69
|
+
File.write(path, new_content)
|
70
|
+
|
71
|
+
strictness_in_content(new_content) == new_strictness
|
72
|
+
end
|
73
|
+
|
74
|
+
# changes the sigil to have a new strictness in a list of files
|
75
|
+
sig { params(path_list: T::Array[String], new_strictness: String).returns(T::Array[String]) }
|
76
|
+
def self.change_sigil_in_files(path_list, new_strictness)
|
77
|
+
path_list.filter do |path|
|
78
|
+
change_sigil_in_file(path, new_strictness)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# finds all files in the specified directory with the passed strictness
|
83
|
+
sig do
|
84
|
+
params(
|
85
|
+
directory: T.any(String, Pathname),
|
86
|
+
strictness: String,
|
87
|
+
extension: String
|
88
|
+
).returns(T::Array[String])
|
89
|
+
end
|
90
|
+
def self.files_with_sigil_strictness(directory, strictness, extension = ".rb")
|
91
|
+
paths = Dir.glob("#{File.expand_path(directory)}/**/*#{extension}").sort.uniq
|
92
|
+
paths.filter do |path|
|
93
|
+
file_strictness(path) == strictness
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "fileutils"
|
5
|
+
require "open3"
|
6
|
+
require "pathname"
|
7
|
+
|
8
|
+
require_relative "../git"
|
9
|
+
|
10
|
+
module Spoom
|
11
|
+
module TestHelpers
|
12
|
+
# A simple project abstraction for testing purposes
|
13
|
+
class Project
|
14
|
+
extend T::Sig
|
15
|
+
|
16
|
+
# The absolute path to this test project
|
17
|
+
sig { returns(String) }
|
18
|
+
attr_reader :path
|
19
|
+
|
20
|
+
# Create a new test project at `path`
|
21
|
+
sig { params(path: String).void }
|
22
|
+
def initialize(path)
|
23
|
+
@path = path
|
24
|
+
FileUtils.rm_rf(@path)
|
25
|
+
FileUtils.mkdir_p(@path)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Content
|
29
|
+
|
30
|
+
# Set the content of the Gemfile in this project
|
31
|
+
sig { params(content: String).void }
|
32
|
+
def gemfile(content)
|
33
|
+
write("Gemfile", content)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Set the content of `sorbet/config` in this project
|
37
|
+
sig { params(content: String).void }
|
38
|
+
def sorbet_config(content)
|
39
|
+
write("sorbet/config", content)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Write `content` in the file at `rel_path`
|
43
|
+
sig { params(rel_path: String, content: String).void }
|
44
|
+
def write(rel_path, content = "")
|
45
|
+
path = absolute_path(rel_path)
|
46
|
+
FileUtils.mkdir_p(File.dirname(path))
|
47
|
+
File.write(path, content)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Remove `rel_path`
|
51
|
+
sig { params(rel_path: String).void }
|
52
|
+
def remove(rel_path)
|
53
|
+
FileUtils.rm_rf(absolute_path(rel_path))
|
54
|
+
end
|
55
|
+
|
56
|
+
# List all files in this project
|
57
|
+
sig { returns(T::Array[String]) }
|
58
|
+
def files
|
59
|
+
Dir.glob("#{@path}/**/*").sort
|
60
|
+
end
|
61
|
+
|
62
|
+
# Actions
|
63
|
+
|
64
|
+
# Run `git init` in this project
|
65
|
+
sig { void }
|
66
|
+
def git_init
|
67
|
+
Spoom::Git.exec("git init -q", path: path)
|
68
|
+
Spoom::Git.exec("git config user.name 'spoom-tests'", path: path)
|
69
|
+
Spoom::Git.exec("git config user.email 'spoom@shopify.com'", path: path)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Commit all new changes in this project
|
73
|
+
sig { params(message: String, date: Time).void }
|
74
|
+
def commit(message = "message", date: Time.now.utc)
|
75
|
+
Spoom::Git.exec("git add --all", path: path)
|
76
|
+
Spoom::Git.exec("GIT_COMMITTER_DATE=\"#{date}\" git commit -m '#{message}' --date '#{date}'", path: path)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Run a command with `bundle exec` in this project
|
80
|
+
sig { params(cmd: String, args: String).returns([T.nilable(String), T.nilable(String), T::Boolean]) }
|
81
|
+
def bundle_exec(cmd, *args)
|
82
|
+
opts = {}
|
83
|
+
opts[:chdir] = path
|
84
|
+
out, err, status = Open3.capture3(["bundle", "exec", cmd, *args].join(' '), opts)
|
85
|
+
[out, err, status.success?]
|
86
|
+
end
|
87
|
+
|
88
|
+
# Delete this project and its content
|
89
|
+
sig { void }
|
90
|
+
def destroy
|
91
|
+
FileUtils.rm_rf(path)
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
# Create an absolute path from `self.path` and `rel_path`
|
97
|
+
sig { params(rel_path: String).returns(String) }
|
98
|
+
def absolute_path(rel_path)
|
99
|
+
(Pathname.new(path) / rel_path).to_s
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative "git"
|
5
|
+
|
6
|
+
module Spoom
|
7
|
+
class Timeline
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig { params(from: Time, to: Time, path: String).void }
|
11
|
+
def initialize(from, to, path: ".")
|
12
|
+
@from = from
|
13
|
+
@to = to
|
14
|
+
@path = path
|
15
|
+
end
|
16
|
+
|
17
|
+
# Return one commit for each month between `from` and `to`
|
18
|
+
sig { returns(T::Array[String]) }
|
19
|
+
def ticks
|
20
|
+
commits_for_dates(months)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return all months between `from` and `to`
|
24
|
+
sig { returns(T::Array[Time]) }
|
25
|
+
def months
|
26
|
+
d = Date.new(@from.year, @from.month, 1)
|
27
|
+
to = Date.new(@to.year, @to.month, 1)
|
28
|
+
res = [d.to_time]
|
29
|
+
while d < to
|
30
|
+
d = d.next_month
|
31
|
+
res << d.to_time
|
32
|
+
end
|
33
|
+
res
|
34
|
+
end
|
35
|
+
|
36
|
+
# Return one commit for each date in `dates`
|
37
|
+
sig { params(dates: T::Array[Time]).returns(T::Array[String]) }
|
38
|
+
def commits_for_dates(dates)
|
39
|
+
dates.map do |t|
|
40
|
+
out, _, _ = Spoom::Git.log(
|
41
|
+
"--since='#{t}'",
|
42
|
+
"--until='#{t.to_date.next_month}'",
|
43
|
+
"--format='format:%h'",
|
44
|
+
"--author-date-order",
|
45
|
+
"-1",
|
46
|
+
path: @path,
|
47
|
+
)
|
48
|
+
next if out.empty?
|
49
|
+
out
|
50
|
+
end.compact.uniq
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/spoom/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: spoom
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexandre Terrasa
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-11-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 13.0.1
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
40
|
+
version: 13.0.1
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: minitest
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -110,7 +110,7 @@ dependencies:
|
|
110
110
|
version: '0'
|
111
111
|
description:
|
112
112
|
email:
|
113
|
-
-
|
113
|
+
- ruby@shopify.com
|
114
114
|
executables:
|
115
115
|
- spoom
|
116
116
|
extensions: []
|
@@ -122,12 +122,24 @@ files:
|
|
122
122
|
- exe/spoom
|
123
123
|
- lib/spoom.rb
|
124
124
|
- lib/spoom/cli.rb
|
125
|
-
- lib/spoom/cli/
|
126
|
-
- lib/spoom/cli/
|
127
|
-
- lib/spoom/cli/
|
128
|
-
- lib/spoom/cli/
|
129
|
-
- lib/spoom/cli/
|
125
|
+
- lib/spoom/cli/bump.rb
|
126
|
+
- lib/spoom/cli/config.rb
|
127
|
+
- lib/spoom/cli/coverage.rb
|
128
|
+
- lib/spoom/cli/helper.rb
|
129
|
+
- lib/spoom/cli/lsp.rb
|
130
|
+
- lib/spoom/cli/run.rb
|
130
131
|
- lib/spoom/config.rb
|
132
|
+
- lib/spoom/coverage.rb
|
133
|
+
- lib/spoom/coverage/d3.rb
|
134
|
+
- lib/spoom/coverage/d3/base.rb
|
135
|
+
- lib/spoom/coverage/d3/circle_map.rb
|
136
|
+
- lib/spoom/coverage/d3/pie.rb
|
137
|
+
- lib/spoom/coverage/d3/timeline.rb
|
138
|
+
- lib/spoom/coverage/report.rb
|
139
|
+
- lib/spoom/coverage/snapshot.rb
|
140
|
+
- lib/spoom/file_tree.rb
|
141
|
+
- lib/spoom/git.rb
|
142
|
+
- lib/spoom/printer.rb
|
131
143
|
- lib/spoom/sorbet.rb
|
132
144
|
- lib/spoom/sorbet/config.rb
|
133
145
|
- lib/spoom/sorbet/errors.rb
|
@@ -136,6 +148,9 @@ files:
|
|
136
148
|
- lib/spoom/sorbet/lsp/errors.rb
|
137
149
|
- lib/spoom/sorbet/lsp/structures.rb
|
138
150
|
- lib/spoom/sorbet/metrics.rb
|
151
|
+
- lib/spoom/sorbet/sigils.rb
|
152
|
+
- lib/spoom/test_helpers/project.rb
|
153
|
+
- lib/spoom/timeline.rb
|
139
154
|
- lib/spoom/version.rb
|
140
155
|
homepage: https://github.com/Shopify/spoom
|
141
156
|
licenses:
|