spoom 1.1.11 → 1.1.13

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b239fa524447d3a3cd6290f8fa373cdaf9a1b3b76bacbbf290519820fe31f542
4
- data.tar.gz: 389b772cafcc6658f6033707ec1696cc1221aab64efc8c0c4bef984e672e1ea4
3
+ metadata.gz: 5cad6017cf76302bc359d170fce6c2e33c4ff5923a564b26e2f270b42c102e3d
4
+ data.tar.gz: 734654483394f8520346d0ddab7872e1c7f42de4eed7523e503264f8045bf2c9
5
5
  SHA512:
6
- metadata.gz: df641a876db23dd5aac45965adbae953ba43ae2fb20720498d6760843212a3541b4d3396401ad9cccb99db58830d3bec7124e32a27372496c61fd896517be126
7
- data.tar.gz: 4f1b7fe4cd814333c449820bbd24f4fd3ca4883986b3d6f3c0c6f59dada06e136114930a9e8e20a72beb85854ef5f240d7dd6d6c0c5c0c28f5b70fc01185045a
6
+ metadata.gz: a67da9fa2811e15d5bfc5f554a2b1cb23bed4a0744617d1168b3d3d4c08f4f6828b207142996fb8cebc57ca8732d901e3e390c43939eaa504a2ed1cc1fe611b4
7
+ data.tar.gz: 1b98af8f37acd554f908fdde1257f9c53eaab0a37b21847e619caee5cdda44e9f06e248545ec247cb43c6ac434d5e00bce4b6731eb434c62bd3a60a72c67313f
data/Gemfile CHANGED
@@ -7,6 +7,7 @@ gemspec
7
7
 
8
8
  group :development do
9
9
  gem "pry-byebug"
10
+ gem "ruby-lsp"
10
11
  gem "rubocop-shopify", require: false
11
12
  gem "rubocop-sorbet", require: false
12
13
  gem "tapioca", require: false
data/README.md CHANGED
@@ -121,6 +121,12 @@ Hide the `Errors: X` at the end of the list:
121
121
  $ spoom tc --no-count
122
122
  ```
123
123
 
124
+ List only the errors comming from specific directories or files:
125
+
126
+ ```
127
+ $ spoom tc file1.rb path1/ path2/
128
+ ```
129
+
124
130
  #### Typing coverage
125
131
 
126
132
  Show metrics about the project contents and the typing coverage:
data/Rakefile CHANGED
@@ -1,5 +1,6 @@
1
1
  # typed: strict
2
2
  # frozen_string_literal: true
3
+
3
4
  require "bundler/gem_tasks"
4
5
  require "rake/testtask"
5
6
 
@@ -1,8 +1,8 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'find'
5
- require 'open3'
4
+ require "find"
5
+ require "open3"
6
6
 
7
7
  module Spoom
8
8
  module Cli
@@ -71,7 +71,7 @@ module Spoom
71
71
  say("\n")
72
72
 
73
73
  if files_to_bump.empty?
74
- say("No file to bump from `#{from}` to `#{to}`")
74
+ say("No files to bump from `#{from}` to `#{to}`")
75
75
  exit(0)
76
76
  end
77
77
 
@@ -85,11 +85,10 @@ module Spoom
85
85
 
86
86
  error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
87
87
  result = Sorbet.srb_tc(
88
- "--no-error-sections",
89
88
  "--error-url-base=#{error_url_base}",
90
89
  path: exec_path,
91
90
  capture_err: true,
92
- sorbet_bin: options[:sorbet]
91
+ sorbet_bin: options[:sorbet],
93
92
  )
94
93
 
95
94
  check_sorbet_segfault(result.exit_code) do
@@ -108,17 +107,22 @@ module Spoom
108
107
 
109
108
  errors = Sorbet::Errors::Parser.parse_string(result.err, error_url_base: error_url_base)
110
109
 
111
- files_with_errors = errors.map do |err|
112
- path = File.expand_path(err.file)
110
+ all_files = errors.flat_map do |err|
111
+ [err.file, *err.files_from_error_sections]
112
+ end
113
+
114
+ files_with_errors = all_files.map do |file|
115
+ path = File.expand_path(file)
113
116
  next unless path.start_with?(directory)
114
117
  next unless File.file?(path)
115
118
  next unless files_to_bump.include?(path)
119
+
116
120
  path
117
121
  end.compact.uniq
118
122
 
119
123
  undo_changes(files_with_errors, from)
120
124
 
121
- say("Found #{errors.length} type checking error#{'s' if errors.length > 1}") if options[:count_errors]
125
+ say("Found #{errors.length} type checking error#{"s" if errors.length > 1}") if options[:count_errors]
122
126
 
123
127
  files_changed = files_to_bump - files_with_errors
124
128
  print_changes(files_changed, command: cmd, from: from, to: to, dry: dry, path: exec_path)
@@ -128,13 +132,14 @@ module Spoom
128
132
 
129
133
  no_commands do
130
134
  def print_changes(files, command:, from: "false", to: "true", dry: false, path: File.expand_path("."))
131
- if files.empty?
132
- say("No file to bump from `#{from}` to `#{to}`")
135
+ files_count = files.size
136
+ if files_count.zero?
137
+ say("No files to bump from `#{from}` to `#{to}`")
133
138
  return
134
139
  end
135
140
  message = StringIO.new
136
141
  message << (dry ? "Can bump" : "Bumped")
137
- message << " `#{files.size}` file#{'s' if files.size > 1}"
142
+ message << " `#{files_count}` file#{"s" if files_count > 1}"
138
143
  message << " from `#{from}` to `#{to}`:"
139
144
  say(message.string)
140
145
  files.each do |file|
@@ -142,7 +147,7 @@ module Spoom
142
147
  say(" + #{file_path}")
143
148
  end
144
149
  if dry && command
145
- say("\nRun `#{command}` to bump them")
150
+ say("\nRun `#{command}` to bump #{files_count > 1 ? "them" : "it"}")
146
151
  elsif dry
147
152
  say("\nRun `spoom bump --from #{from} --to #{to}` locally then `commit the changes` and `push them`")
148
153
  end
@@ -1,8 +1,8 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative '../coverage'
5
- require_relative '../timeline'
4
+ require_relative "../coverage"
5
+ require_relative "../timeline"
6
6
 
7
7
  module Spoom
8
8
  module Cli
@@ -27,6 +27,7 @@ module Spoom
27
27
 
28
28
  save_dir = options[:save]
29
29
  return unless save_dir
30
+
30
31
  FileUtils.mkdir_p(save_dir)
31
32
  file = "#{save_dir}/#{snapshot.commit_sha || snapshot.timestamp}.json"
32
33
  File.write(file, snapshot.to_json)
@@ -45,7 +46,7 @@ module Spoom
45
46
  sorbet = options[:sorbet]
46
47
 
47
48
  ref_before = Spoom::Git.current_branch
48
- ref_before = Spoom::Git.last_commit(path: path) unless ref_before
49
+ ref_before = Spoom::Git.last_commit(path: path)&.sha unless ref_before
49
50
  unless ref_before
50
51
  say_error("Not in a git repository")
51
52
  say_error("\nSpoom needs to checkout into your previous commits to build the timeline.", status: nil)
@@ -70,9 +71,9 @@ module Spoom
70
71
  to = parse_time(options[:to], "--to")
71
72
 
72
73
  unless from
73
- intro_sha = Spoom::Git.sorbet_intro_commit(path: path)
74
- intro_sha = T.must(intro_sha) # we know it's in there since in_sorbet_project!
75
- from = Spoom::Git.commit_time(intro_sha, path: path)
74
+ intro_commit = Spoom::Git.sorbet_intro_commit(path: path)
75
+ intro_commit = T.must(intro_commit) # we know it's in there since in_sorbet_project!
76
+ from = intro_commit.time
76
77
  end
77
78
 
78
79
  timeline = Spoom::Timeline.new(from, to, path: path)
@@ -83,16 +84,16 @@ module Spoom
83
84
  exit(1)
84
85
  end
85
86
 
86
- ticks.each_with_index do |sha, i|
87
- date = Spoom::Git.commit_time(sha, path: path)
88
- say("Analyzing commit `#{sha}` - #{date&.strftime('%F')} (#{i + 1} / #{ticks.size})")
87
+ ticks.each_with_index do |commit, i|
88
+ say("Analyzing commit `#{commit.sha}` - #{commit.time.strftime("%F")} (#{i + 1} / #{ticks.size})")
89
89
 
90
- Spoom::Git.checkout(sha, path: path)
90
+ Spoom::Git.checkout(commit.sha, path: path)
91
91
 
92
92
  snapshot = T.let(nil, T.nilable(Spoom::Coverage::Snapshot))
93
93
  if options[:bundle_install]
94
94
  Bundler.with_unbundled_env do
95
- next unless bundle_install(path, sha)
95
+ next unless bundle_install(path, commit.sha)
96
+
96
97
  snapshot = Spoom::Coverage.snapshot(path: path, sorbet_bin: sorbet)
97
98
  end
98
99
  else
@@ -104,7 +105,8 @@ module Spoom
104
105
  say("\n")
105
106
 
106
107
  next unless save_dir
107
- file = "#{save_dir}/#{sha}.json"
108
+
109
+ file = "#{save_dir}/#{commit.sha}.json"
108
110
  File.write(file, snapshot.to_json)
109
111
  say(" Snapshot data saved under `#{file}`\n\n")
110
112
  end
@@ -145,7 +147,7 @@ module Spoom
145
147
  false: options[:color_false],
146
148
  true: options[:color_true],
147
149
  strict: options[:color_strict],
148
- strong: options[:color_strong]
150
+ strong: options[:color_strong],
149
151
  )
150
152
 
151
153
  report = Spoom::Coverage.report(snapshots, palette: palette, path: exec_path)
@@ -161,9 +163,9 @@ module Spoom
161
163
  say_error("No report file to open `#{file}`")
162
164
  say_error(<<~ERR, status: nil)
163
165
 
164
- If you already generated a report under another name use #{blue('spoom coverage open PATH')}.
166
+ If you already generated a report under another name use #{blue("spoom coverage open PATH")}.
165
167
 
166
- To generate a report run #{blue('spoom coverage report')}.
168
+ To generate a report run #{blue("spoom coverage report")}.
167
169
  ERR
168
170
  exit(1)
169
171
  end
@@ -174,6 +176,7 @@ module Spoom
174
176
  no_commands do
175
177
  def parse_time(string, option)
176
178
  return nil unless string
179
+
177
180
  Time.parse(string)
178
181
  rescue ArgumentError
179
182
  say_error("Invalid date `#{string}` for option `#{option}` (expected format `YYYY-MM-DD`)")
@@ -196,9 +199,9 @@ module Spoom
196
199
  say_error("No snapshot files found in `#{file}`")
197
200
  say_error(<<~ERR, status: nil)
198
201
 
199
- If you already generated snapshot files under another directory use #{blue('spoom coverage report PATH')}.
202
+ If you already generated snapshot files under another directory use #{blue("spoom coverage report PATH")}.
200
203
 
201
- To generate snapshot files run #{blue('spoom coverage timeline --save-dir spoom_data')}.
204
+ To generate snapshot files run #{blue("spoom coverage timeline --save")}.
202
205
  ERR
203
206
  end
204
207
  end
@@ -33,7 +33,7 @@ module Spoom
33
33
  params(
34
34
  message: String,
35
35
  status: T.nilable(String),
36
- nl: T::Boolean
36
+ nl: T::Boolean,
37
37
  ).void
38
38
  end
39
39
  def say_error(message, status: "Error", nl: true)
@@ -60,8 +60,8 @@ module Spoom
60
60
  unless in_sorbet_project?
61
61
  say_error(
62
62
  "not in a Sorbet project (`#{sorbet_config_file}` not found)\n\n" \
63
- "When running spoom from another path than the project's root, " \
64
- "use `--path PATH` to specify the path to the root."
63
+ "When running spoom from another path than the project's root, " \
64
+ "use `--path PATH` to specify the path to the root.",
65
65
  )
66
66
  Kernel.exit(1)
67
67
  end
@@ -116,9 +116,9 @@ module Spoom
116
116
  word = StringIO.new
117
117
  in_ticks = T.let(false, T::Boolean)
118
118
  string.chars.each do |c|
119
- if c == '`' && !in_ticks
119
+ if c == "`" && !in_ticks
120
120
  in_ticks = true
121
- elsif c == '`' && in_ticks
121
+ elsif c == "`" && in_ticks
122
122
  in_ticks = false
123
123
  res << colorize(word.string, HIGHLIGHT_COLOR)
124
124
  word = StringIO.new
@@ -135,6 +135,7 @@ module Spoom
135
135
  sig { params(string: String, color: Color).returns(String) }
136
136
  def colorize(string, *color)
137
137
  return string unless color?
138
+
138
139
  T.unsafe(self).set_color(string, *color)
139
140
  end
140
141
 
data/lib/spoom/cli/lsp.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # typed: true
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'shellwords'
4
+ require "shellwords"
5
5
 
6
6
  require_relative "../sorbet/lsp"
7
7
 
@@ -28,6 +28,7 @@ module Spoom
28
28
  Dir["**/*.rb"].each do |file|
29
29
  res = client.document_symbols(to_uri(file))
30
30
  next if res.empty?
31
+
31
32
  say("Symbols from `#{file}`:")
32
33
  printer.print_objects(res)
33
34
  end
@@ -117,7 +118,7 @@ module Spoom
117
118
  "--lsp",
118
119
  "--enable-all-experimental-lsp-features",
119
120
  "--disable-watchman",
120
- path: path
121
+ path: path,
121
122
  )
122
123
  client.open(File.expand_path(path))
123
124
  client
@@ -127,7 +128,7 @@ module Spoom
127
128
  Spoom::LSP::SymbolPrinter.new(
128
129
  indent_level: 2,
129
130
  colors: options[:color],
130
- prefix: "file://#{File.expand_path(exec_path)}"
131
+ prefix: "file://#{File.expand_path(exec_path)}",
131
132
  )
132
133
  end
133
134
 
data/lib/spoom/cli/run.rb CHANGED
@@ -22,7 +22,8 @@ module Spoom
22
22
  option :uniq, type: :boolean, aliases: :u, desc: "Remove duplicated lines"
23
23
  option :count, type: :boolean, default: true, desc: "Show errors count"
24
24
  option :sorbet, type: :string, desc: "Path to custom Sorbet bin"
25
- def tc(*arg)
25
+ option :sorbet_options, type: :string, default: "", desc: "Pass options to Sorbet"
26
+ def tc(*paths_to_select)
26
27
  in_sorbet_project!
27
28
 
28
29
  path = exec_path
@@ -36,10 +37,10 @@ module Spoom
36
37
 
37
38
  unless limit || code || sort
38
39
  result = T.unsafe(Spoom::Sorbet).srb_tc(
39
- *arg,
40
+ *options[:sorbet_options].split(" "),
40
41
  path: path,
41
42
  capture_err: false,
42
- sorbet_bin: sorbet
43
+ sorbet_bin: sorbet,
43
44
  )
44
45
 
45
46
  check_sorbet_segfault(result.code)
@@ -47,11 +48,13 @@ module Spoom
47
48
  exit(result.status)
48
49
  end
49
50
 
51
+ error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE
50
52
  result = T.unsafe(Spoom::Sorbet).srb_tc(
51
- *arg,
53
+ *options[:sorbet_options].split(" "),
54
+ "--error-url-base=#{error_url_base}",
52
55
  path: path,
53
56
  capture_err: true,
54
- sorbet_bin: sorbet
57
+ sorbet_bin: sorbet,
55
58
  )
56
59
 
57
60
  check_sorbet_segfault(result.exit_code)
@@ -61,9 +64,17 @@ module Spoom
61
64
  exit(0)
62
65
  end
63
66
 
64
- errors = Spoom::Sorbet::Errors::Parser.parse_string(result.err)
67
+ errors = Spoom::Sorbet::Errors::Parser.parse_string(result.err, error_url_base: error_url_base)
65
68
  errors_count = errors.size
66
69
 
70
+ errors = errors.select { |e| e.code == code } if code
71
+
72
+ unless paths_to_select.empty?
73
+ errors.select! do |error|
74
+ paths_to_select.any? { |path_to_select| error.file&.start_with?(path_to_select) }
75
+ end
76
+ end
77
+
67
78
  errors = case sort
68
79
  when SORT_CODE
69
80
  Spoom::Sorbet::Errors.sort_errors_by_code(errors)
@@ -73,7 +84,6 @@ module Spoom
73
84
  errors # preserve natural sort
74
85
  end
75
86
 
76
- errors = errors.select { |e| e.code == code } if code
77
87
  errors = T.must(errors.slice(0, limit)) if limit
78
88
 
79
89
  lines = errors.map { |e| format_error(e, format || DEFAULT_FORMAT) }
@@ -110,7 +120,7 @@ module Spoom
110
120
  cyan = T.let(false, T::Boolean)
111
121
  word = StringIO.new
112
122
  message.chars.each do |c|
113
- if c == '`'
123
+ if c == "`"
114
124
  cyan = !cyan
115
125
  next
116
126
  end
data/lib/spoom/cli.rb CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  require "thor"
5
5
 
6
- require_relative 'cli/helper'
6
+ require_relative "cli/helper"
7
7
 
8
8
  require_relative "cli/bump"
9
9
  require_relative "cli/config"
@@ -20,7 +20,7 @@ module Spoom
20
20
  class_option :color, type: :boolean, default: true, desc: "Use colors"
21
21
  class_option :path, type: :string, default: ".", aliases: :p, desc: "Run spoom in a specific path"
22
22
 
23
- map T.unsafe(%w[--version -v] => :__print_version)
23
+ map T.unsafe(["--version", "-v"] => :__print_version)
24
24
 
25
25
  desc "bump", "Bump Sorbet sigils from `false` to `true` when no errors"
26
26
  subcommand "bump", Spoom::Cli::Bump
@@ -71,8 +71,10 @@ module Spoom
71
71
 
72
72
  # Utils
73
73
 
74
- def self.exit_on_failure?
75
- true
74
+ class << self
75
+ def exit_on_failure?
76
+ true
77
+ end
76
78
  end
77
79
  end
78
80
  end
@@ -0,0 +1,219 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require "open3"
6
+
7
+ module Spoom
8
+ # An abstraction to a Ruby project context
9
+ #
10
+ # A context maps to a directory in the file system.
11
+ # It is used to manipulate files and run commands in the context of this directory.
12
+ class Context
13
+ extend T::Sig
14
+
15
+ # The absolute path to the directory this context is about
16
+ sig { returns(String) }
17
+ attr_reader :absolute_path
18
+
19
+ class << self
20
+ extend T::Sig
21
+
22
+ # Create a new context in the system's temporary directory
23
+ #
24
+ # `name` is used as prefix to the temporary directory name.
25
+ # The directory will be created if it doesn't exist.
26
+ sig { params(name: T.nilable(String)).returns(T.attached_class) }
27
+ def mktmp!(name = nil)
28
+ new(::Dir.mktmpdir(name))
29
+ end
30
+ end
31
+
32
+ # Create a new context about `absolute_path`
33
+ #
34
+ # The directory will not be created if it doesn't exist.
35
+ # Call `#make!` to create it.
36
+ sig { params(absolute_path: String).void }
37
+ def initialize(absolute_path)
38
+ @absolute_path = T.let(::File.expand_path(absolute_path), String)
39
+ end
40
+
41
+ # Returns the absolute path to `relative_path` in the context's directory
42
+ sig { params(relative_path: String).returns(String) }
43
+ def absolute_path_to(relative_path)
44
+ File.join(@absolute_path, relative_path)
45
+ end
46
+
47
+ # File System
48
+
49
+ # Create the context directory at `absolute_path`
50
+ sig { void }
51
+ def mkdir!
52
+ FileUtils.rm_rf(@absolute_path)
53
+ FileUtils.mkdir_p(@absolute_path)
54
+ end
55
+
56
+ # List all files in this context matching `pattern`
57
+ sig { params(pattern: String).returns(T::Array[String]) }
58
+ def glob(pattern = "**/*")
59
+ Dir.glob(absolute_path_to(pattern)).map do |path|
60
+ Pathname.new(path).relative_path_from(@absolute_path).to_s
61
+ end.sort
62
+ end
63
+
64
+ # List all files at the top level of this context directory
65
+ sig { returns(T::Array[String]) }
66
+ def list
67
+ glob("*")
68
+ end
69
+
70
+ # Does `relative_path` point to an existing file in this context directory?
71
+ sig { params(relative_path: String).returns(T::Boolean) }
72
+ def file?(relative_path)
73
+ File.file?(absolute_path_to(relative_path))
74
+ end
75
+
76
+ # Return the contents of the file at `relative_path` in this context directory
77
+ #
78
+ # Will raise if the file doesn't exist.
79
+ sig { params(relative_path: String).returns(String) }
80
+ def read(relative_path)
81
+ File.read(absolute_path_to(relative_path))
82
+ end
83
+
84
+ # Write `contents` in the file at `relative_path` in this context directory
85
+ #
86
+ # Append to the file if `append` is true.
87
+ sig { params(relative_path: String, contents: String, append: T::Boolean).void }
88
+ def write!(relative_path, contents = "", append: false)
89
+ absolute_path = absolute_path_to(relative_path)
90
+ FileUtils.mkdir_p(File.dirname(absolute_path))
91
+ File.write(absolute_path, contents, mode: append ? "a" : "w")
92
+ end
93
+
94
+ # Remove the path at `relative_path` (recursive + force) in this context directory
95
+ sig { params(relative_path: String).void }
96
+ def remove!(relative_path)
97
+ FileUtils.rm_rf(absolute_path_to(relative_path))
98
+ end
99
+
100
+ # Move the file or directory from `from_relative_path` to `to_relative_path`
101
+ sig { params(from_relative_path: String, to_relative_path: String).void }
102
+ def move!(from_relative_path, to_relative_path)
103
+ destination_path = absolute_path_to(to_relative_path)
104
+ FileUtils.mkdir_p(File.dirname(destination_path))
105
+ FileUtils.mv(absolute_path_to(from_relative_path), destination_path)
106
+ end
107
+
108
+ # Delete this context and its content
109
+ #
110
+ # Warning: it will `rm -rf` the context directory on the file system.
111
+ sig { void }
112
+ def destroy!
113
+ FileUtils.rm_rf(@absolute_path)
114
+ end
115
+
116
+ # Execution
117
+
118
+ # Run a command in this context directory
119
+ sig { params(command: String, capture_err: T::Boolean).returns(ExecResult) }
120
+ def exec(command, capture_err: true)
121
+ Bundler.with_unbundled_env do
122
+ opts = T.let({ chdir: @absolute_path }, T::Hash[Symbol, T.untyped])
123
+
124
+ if capture_err
125
+ out, err, status = Open3.capture3(command, opts)
126
+ ExecResult.new(out: out, err: err, status: T.must(status.success?), exit_code: T.must(status.exitstatus))
127
+ else
128
+ out, status = Open3.capture2(command, opts)
129
+ ExecResult.new(out: out, err: "", status: T.must(status.success?), exit_code: T.must(status.exitstatus))
130
+ end
131
+ end
132
+ end
133
+
134
+ # Bundle
135
+
136
+ # Read the `contents` of the Gemfile in this context directory
137
+ sig { returns(T.nilable(String)) }
138
+ def read_gemfile
139
+ read("Gemfile")
140
+ end
141
+
142
+ # Set the `contents` of the Gemfile in this context directory
143
+ sig { params(contents: String, append: T::Boolean).void }
144
+ def write_gemfile!(contents, append: false)
145
+ write!("Gemfile", contents, append: append)
146
+ end
147
+
148
+ # Run a command with `bundle` in this context directory
149
+ sig { params(command: String, version: T.nilable(String)).returns(ExecResult) }
150
+ def bundle(command, version: nil)
151
+ command = "_#{version}_ #{command}" if version
152
+ exec("bundle #{command}")
153
+ end
154
+
155
+ # Run `bundle install` in this context directory
156
+ sig { params(version: T.nilable(String)).returns(ExecResult) }
157
+ def bundle_install!(version: nil)
158
+ bundle("install", version: version)
159
+ end
160
+
161
+ # Run a command `bundle exec` in this context directory
162
+ sig { params(command: String, version: T.nilable(String)).returns(ExecResult) }
163
+ def bundle_exec(command, version: nil)
164
+ bundle("exec #{command}", version: version)
165
+ end
166
+
167
+ # Git
168
+
169
+ # Run a command prefixed by `git` in this context directory
170
+ sig { params(command: String).returns(ExecResult) }
171
+ def git(command)
172
+ exec("git #{command}")
173
+ end
174
+
175
+ # Run `git init` in this context directory
176
+ sig { params(branch: String).void }
177
+ def git_init!(branch: "main")
178
+ git("init -q -b #{branch}")
179
+ end
180
+
181
+ # Run `git checkout` in this context directory
182
+ sig { params(ref: String).returns(ExecResult) }
183
+ def git_checkout!(ref: "main")
184
+ git("checkout #{ref}")
185
+ end
186
+
187
+ # Get the current git branch in this context directory
188
+ sig { returns(T.nilable(String)) }
189
+ def git_current_branch
190
+ Spoom::Git.current_branch(path: @absolute_path)
191
+ end
192
+
193
+ # Sorbet
194
+
195
+ # Run `bundle exec srb` in this context directory
196
+ sig { params(command: String).returns(ExecResult) }
197
+ def srb(command)
198
+ bundle_exec("srb #{command}")
199
+ end
200
+
201
+ # Read the contents of `sorbet/config` in this context directory
202
+ sig { returns(String) }
203
+ def read_sorbet_config
204
+ read(Spoom::Sorbet::CONFIG_PATH)
205
+ end
206
+
207
+ # Set the `contents` of `sorbet/config` in this context directory
208
+ sig { params(contents: String, append: T::Boolean).void }
209
+ def write_sorbet_config!(contents, append: false)
210
+ write!(Spoom::Sorbet::CONFIG_PATH, contents, append: append)
211
+ end
212
+
213
+ # Read the strictness sigil from the file at `relative_path` (returns `nil` if no sigil)
214
+ sig { params(relative_path: String).returns(T.nilable(String)) }
215
+ def read_file_strictness(relative_path)
216
+ Spoom::Sorbet::Sigils.file_strictness(absolute_path_to(relative_path))
217
+ end
218
+ end
219
+ end
@@ -19,14 +19,18 @@ module Spoom
19
19
  @data = data
20
20
  end
21
21
 
22
- sig { returns(String) }
23
- def self.header_style
24
- ""
25
- end
26
-
27
- sig { returns(String) }
28
- def self.header_script
29
- ""
22
+ class << self
23
+ extend T::Sig
24
+
25
+ sig { returns(String) }
26
+ def header_style
27
+ ""
28
+ end
29
+
30
+ sig { returns(String) }
31
+ def header_script
32
+ ""
33
+ end
30
34
  end
31
35
 
32
36
  sig { returns(String) }