spoom 1.0.1 → 1.0.6

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.
@@ -0,0 +1,33 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "sigils"
5
+
6
+ module Spoom
7
+ module Sorbet
8
+ module MetricsParser
9
+ extend T::Sig
10
+
11
+ DEFAULT_PREFIX = "ruby_typer.unknown.."
12
+
13
+ sig { params(path: String, prefix: String).returns(T::Hash[String, Integer]) }
14
+ def self.parse_file(path, prefix = DEFAULT_PREFIX)
15
+ parse_string(File.read(path), prefix)
16
+ end
17
+
18
+ sig { params(string: String, prefix: String).returns(T::Hash[String, Integer]) }
19
+ def self.parse_string(string, prefix = DEFAULT_PREFIX)
20
+ parse_hash(JSON.parse(string), prefix)
21
+ end
22
+
23
+ sig { params(obj: T::Hash[String, T.untyped], prefix: String).returns(T::Hash[String, Integer]) }
24
+ def self.parse_hash(obj, prefix = DEFAULT_PREFIX)
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
29
+ end
30
+ end
31
+ end
32
+ end
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
@@ -1,5 +1,6 @@
1
+ # typed: true
1
2
  # frozen_string_literal: true
2
3
 
3
4
  module Spoom
4
- VERSION = "1.0.1"
5
+ VERSION = "1.0.6"
5
6
  end
@@ -0,0 +1,8 @@
1
+ <div class="card">
2
+ <% if title %>
3
+ <h5 class="card-header"><%= title %></h5>
4
+ <% end %>
5
+ <div class="card-body">
6
+ <%= body %>
7
+ </div>
8
+ </div>
@@ -0,0 +1,22 @@
1
+ <div class="card">
2
+ <% if title %>
3
+ <h5 class="card-header"><%= title %></h5>
4
+ <% end %>
5
+ <div class="card-body">
6
+ <div class="container-fluid">
7
+ <div class="row justify-content-md-center">
8
+ <div class="col-12 col-sm-4 col-xl-3">
9
+ <%= pie_sigils.html %>
10
+ </div>
11
+ <div class="d-none d-xl-block col-xl-1"></div>
12
+ <div class="col-12 col-sm-4 col-xl-3">
13
+ <%= pie_calls.html %>
14
+ </div>
15
+ <div class="d-none d-xl-block col-xl-1"></div>
16
+ <div class="col-12 col-sm-4 col-xl-3">
17
+ <%= pie_sigs.html %>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </div>
@@ -0,0 +1,50 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta http-equiv="x-ua-compatible" content="ie=edge" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+
8
+ <title><%= title %></title>
9
+ <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
10
+
11
+ <style>
12
+ .card {
13
+ margin: 30px 0 0 0;
14
+ }
15
+
16
+ .footer {
17
+ color: #6c757d;
18
+ text-align: center;
19
+ margin: 30px 0 0 0;
20
+ }
21
+
22
+ <%= header_style %>
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
27
+ <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js"></script>
28
+ <script src="https://d3js.org/d3.v4.min.js"></script>
29
+
30
+ <script>
31
+ <%= header_script %>
32
+ </script>
33
+
34
+ <div class="px-5 py-4 container-fluid">
35
+ <div class="row justify-content-center">
36
+ <div class="col-xs-12 col-md-12 col-lg-9 col-xl-8">
37
+ <div class="header">
38
+ <%= header_html %>
39
+ </div>
40
+ <div class="body">
41
+ <%= body_html %>
42
+ </div>
43
+ <div class="footer">
44
+ <%= footer_html %>
45
+ </div>
46
+ </div>
47
+ </div>
48
+ </div>
49
+ </body>
50
+ </html>
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spoom
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.6
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-07-07 00:00:00.000000000 Z
11
+ date: 2020-11-19 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: sorbet-runtime
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: bundler
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -44,14 +30,14 @@ dependencies:
44
30
  requirements:
45
31
  - - "~>"
46
32
  - !ruby/object:Gem::Version
47
- version: '10.0'
33
+ version: 13.0.1
48
34
  type: :development
49
35
  prerelease: false
50
36
  version_requirements: !ruby/object:Gem::Requirement
51
37
  requirements:
52
38
  - - "~>"
53
39
  - !ruby/object:Gem::Version
54
- version: '10.0'
40
+ version: 13.0.1
55
41
  - !ruby/object:Gem::Dependency
56
42
  name: minitest
57
43
  requirement: !ruby/object:Gem::Requirement
@@ -66,6 +52,20 @@ dependencies:
66
52
  - - "~>"
67
53
  - !ruby/object:Gem::Version
68
54
  version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: sorbet-runtime
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: sorbet
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -80,9 +80,37 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: 0.5.5
83
+ - !ruby/object:Gem::Dependency
84
+ name: thor
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 0.19.2
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 0.19.2
97
+ - !ruby/object:Gem::Dependency
98
+ name: colorize
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
83
111
  description:
84
112
  email:
85
- - alexandre.terrasa@shopify.com
113
+ - ruby@shopify.com
86
114
  executables:
87
115
  - spoom
88
116
  extensions: []
@@ -93,8 +121,40 @@ files:
93
121
  - Rakefile
94
122
  - exe/spoom
95
123
  - lib/spoom.rb
124
+ - lib/spoom/cli.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
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
143
+ - lib/spoom/sorbet.rb
96
144
  - lib/spoom/sorbet/config.rb
145
+ - lib/spoom/sorbet/errors.rb
146
+ - lib/spoom/sorbet/lsp.rb
147
+ - lib/spoom/sorbet/lsp/base.rb
148
+ - lib/spoom/sorbet/lsp/errors.rb
149
+ - lib/spoom/sorbet/lsp/structures.rb
150
+ - lib/spoom/sorbet/metrics.rb
151
+ - lib/spoom/sorbet/sigils.rb
152
+ - lib/spoom/test_helpers/project.rb
153
+ - lib/spoom/timeline.rb
97
154
  - lib/spoom/version.rb
155
+ - templates/card.erb
156
+ - templates/card_snapshot.erb
157
+ - templates/page.erb
98
158
  homepage: https://github.com/Shopify/spoom
99
159
  licenses:
100
160
  - MIT
@@ -108,7 +168,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
108
168
  requirements:
109
169
  - - ">="
110
170
  - !ruby/object:Gem::Version
111
- version: 2.6.3
171
+ version: 2.3.7
112
172
  required_rubygems_version: !ruby/object:Gem::Requirement
113
173
  requirements:
114
174
  - - ">="