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.
@@ -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
- class Metrics < T::Struct
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(Metrics) }
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(Metrics) }
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(Metrics) }
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
- Metrics.new(
33
- repo: obj.fetch("repo"),
34
- sha: obj.fetch("sha"),
35
- status: obj.fetch("status"),
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
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Spoom
5
- VERSION = "1.0.4"
5
+ VERSION = "1.0.5"
6
6
  end
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
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-08-05 00:00:00.000000000 Z
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: '10.0'
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: '10.0'
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
- - alexandre.terrasa@shopify.com
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/commands/base.rb
126
- - lib/spoom/cli/commands/config.rb
127
- - lib/spoom/cli/commands/lsp.rb
128
- - lib/spoom/cli/commands/run.rb
129
- - lib/spoom/cli/symbol_printer.rb
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: