spoom 1.0.0 → 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.
@@ -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.0"
5
+ VERSION = "1.0.5"
5
6
  end
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.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-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
@@ -81,22 +81,36 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: 0.5.5
83
83
  - !ruby/object:Gem::Dependency
84
- name: tapioca
84
+ name: thor
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - "~>"
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: '0.4'
89
+ version: 0.19.2
90
90
  type: :runtime
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - "~>"
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
+ - - ">="
95
102
  - !ruby/object:Gem::Version
96
- version: '0.4'
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'
97
111
  description:
98
112
  email:
99
- - alexandre.terrasa@shopify.com
113
+ - ruby@shopify.com
100
114
  executables:
101
115
  - spoom
102
116
  extensions: []
@@ -107,7 +121,36 @@ files:
107
121
  - Rakefile
108
122
  - exe/spoom
109
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
110
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
111
154
  - lib/spoom/version.rb
112
155
  homepage: https://github.com/Shopify/spoom
113
156
  licenses:
@@ -122,7 +165,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
122
165
  requirements:
123
166
  - - ">="
124
167
  - !ruby/object:Gem::Version
125
- version: 2.6.3
168
+ version: 2.3.7
126
169
  required_rubygems_version: !ruby/object:Gem::Requirement
127
170
  requirements:
128
171
  - - ">="