diff --git a/lib/spoom/cli/srb/bump.rb b/lib/spoom/cli/srb/bump.rb index dfa369a8..0e3b6c60 100644 --- a/lib/spoom/cli/srb/bump.rb +++ b/lib/spoom/cli/srb/bump.rb @@ -3,6 +3,8 @@ require "find" require "open3" +require "securerandom" +require "yaml" module Spoom module Cli @@ -72,6 +74,11 @@ def bump(directory = ".") exit(1) end + unless context.sorbet_config.typed_overrides.empty? + say_error("Cannot run `spoom bump` on a project that already uses Sorbet's `--typed-override` option") + exit(1) + end + say("Checking files...") files_to_bump = context.srb_files_with_strictness(from, include_rbis: false) @@ -90,18 +97,18 @@ def bump(directory = ".") exit(0) end - Sorbet::Sigils.change_sigil_in_files(files_to_bump, to) - if force + Sorbet::Sigils.change_sigil_in_files(files_to_bump, to) unless dry print_changes(files_to_bump, command: cmd, from: from, to: to, dry: dry, path: exec_path) - undo_changes(files_to_bump, from) if dry exit(files_to_bump.empty?) end error_url_base = Spoom::Sorbet::Errors::DEFAULT_ERROR_URL_BASE + typed_override_file = write_typed_override_file(context, files_to_bump, to) result = begin T.unsafe(context).srb_tc( *options[:sorbet_options].split(" "), + "--typed-override=#{typed_override_file}", "--error-url-base=#{error_url_base}", capture_err: true, sorbet_bin: options[:sorbet], @@ -114,7 +121,6 @@ def bump(directory = ".") It means one of the file bumped to `typed: #{to}` made Sorbet crash. Run `spoom bump -f` locally followed by `bundle exec srb tc` to investigate the problem. ERR - undo_changes(files_to_bump, from) exit(error.result.exit_code) rescue Spoom::Sorbet::Error::Killed => error say_error(<<~ERR, status: nil) @@ -123,13 +129,14 @@ def bump(directory = ".") It means Sorbet was killed while executing. Changes to files have not been applied. Re-run `spoom bump` to try again. ERR - undo_changes(files_to_bump, from) exit(error.result.exit_code) + ensure + context.remove!(typed_override_file) end if result.status + Sorbet::Sigils.change_sigil_in_files(files_to_bump, to) unless dry print_changes(files_to_bump, command: cmd, from: from, to: to, dry: dry, path: exec_path) - undo_changes(files_to_bump, from) if dry exit(files_to_bump.empty?) end @@ -137,7 +144,6 @@ def bump(directory = ".") # Sorbet will return exit code 100 if there are type checking errors. # If Sorbet returned something else, it means it didn't terminate normally. say_error(result.err, status: nil, nl: false) - undo_changes(files_to_bump, from) exit(1) end @@ -156,13 +162,11 @@ def bump(directory = ".") path end.compact.uniq - undo_changes(files_with_errors, from) - say("Found #{errors.length} type checking error#{"s" if errors.length > 1}") if options[:count_errors] files_changed = files_to_bump - files_with_errors + Sorbet::Sigils.change_sigil_in_files(files_changed, to) unless dry print_changes(files_changed, command: cmd, from: from, to: to, dry: dry, path: exec_path) - undo_changes(files_to_bump, from) if dry exit(files_changed.empty?) end @@ -189,8 +193,15 @@ def print_changes(files, command:, from: "false", to: "true", dry: false, path: end end - def undo_changes(files, from_strictness) - Sorbet::Sigils.change_sigil_in_files(files, from_strictness) + #: (Spoom::Context context, Array[String] files, String strictness) -> String + def write_typed_override_file(context, files, strictness) + path = ".spoom-typed-override-#{Process.pid}-#{SecureRandom.hex(8)}.yaml" + relative_paths = files.map do |file| + relative_path = file.delete_prefix("#{context.absolute_path}/") + "./#{relative_path}" + end + context.write!(path, YAML.dump({ strictness => relative_paths })) + path end end end diff --git a/lib/spoom/sorbet/config.rb b/lib/spoom/sorbet/config.rb index 14b15ce5..010a71e2 100644 --- a/lib/spoom/sorbet/config.rb +++ b/lib/spoom/sorbet/config.rb @@ -27,7 +27,7 @@ class Config DEFAULT_ALLOWED_EXTENSIONS = [".rb", ".rbi"].freeze #: Array[String] #: Array[String] - attr_accessor :paths, :ignore, :allowed_extensions + attr_accessor :paths, :ignore, :allowed_extensions, :typed_overrides #: bool attr_accessor :no_stdlib @@ -37,6 +37,7 @@ def initialize @paths = [] #: Array[String] @ignore = [] #: Array[String] @allowed_extensions = [] #: Array[String] + @typed_overrides = [] #: Array[String] @no_stdlib = false #: bool end @@ -46,6 +47,7 @@ def initialize_copy(source) @paths = @paths.dup @ignore = @ignore.dup @allowed_extensions = @allowed_extensions.dup + @typed_overrides = @typed_overrides.dup end # Returns self as a string of options that can be passed to Sorbet @@ -66,6 +68,7 @@ def options_string opts.concat(paths.map { |p| "'#{p}'" }) opts.concat(ignore.map { |p| "--ignore '#{p}'" }) opts.concat(allowed_extensions.map { |ext| "--allowed-extension '#{ext}'" }) + opts.concat(typed_overrides.map { |path| "--typed-override '#{path}'" }) opts << "--no-stdlib" if @no_stdlib opts.join(" ") end @@ -95,6 +98,12 @@ def parse_string(sorbet_config) when /^--ignore=/ config.ignore << parse_option(line) next + when /^--typed-override$/ + state = :typed_override + next + when /^--typed-override=/ + config.typed_overrides << parse_option(line) + next when /^--file$/ next when /^--file=/ @@ -124,6 +133,8 @@ def parse_string(sorbet_config) config.ignore << line when :extension config.allowed_extensions << line + when :typed_override + config.typed_overrides << line when :skip # nothing else diff --git a/rbi/spoom.rbi b/rbi/spoom.rbi index 254e5f03..7c3787f4 100644 --- a/rbi/spoom.rbi +++ b/rbi/spoom.rbi @@ -128,7 +128,9 @@ class Spoom::Cli::Srb::Bump < ::Thor def help(command = T.unsafe(nil), subcommand = T.unsafe(nil)); end def print_changes(files, command:, from: T.unsafe(nil), to: T.unsafe(nil), dry: T.unsafe(nil), path: T.unsafe(nil)); end - def undo_changes(files, from_strictness); end + + sig { params(context: ::Spoom::Context, files: T::Array[::String], strictness: ::String).returns(::String) } + def write_typed_override_file(context, files, strictness); end end class Spoom::Cli::Srb::Coverage < ::Thor @@ -2661,6 +2663,8 @@ class Spoom::Sorbet::Config def paths; end def paths=(_arg0); end + def typed_overrides; end + def typed_overrides=(_arg0); end private diff --git a/test/spoom/cli/srb/bump_test.rb b/test/spoom/cli/srb/bump_test.rb index ee548599..5035bb56 100644 --- a/test/spoom/cli/srb/bump_test.rb +++ b/test/spoom/cli/srb/bump_test.rb @@ -11,6 +11,12 @@ def setup @project.bundle_install! end + def teardown + assert_empty(Dir.glob("#{@project.absolute_path}/.spoom-typed-override-*")) + ensure + super + end + def test_bump_outside_sorbet_dir @project.remove!("sorbet/config") result = @project.spoom("srb bump --no-color") @@ -54,6 +60,30 @@ class A; end assert_equal("false", @project.read_file_strictness("file2.rb")) end + def test_bump_errors_if_sorbet_config_has_typed_override + @project.write_sorbet_config!(<<~CONFIG) + . + --typed-override=typed-override.yaml + CONFIG + @project.write!("typed-override.yaml", <<~YAML) + true: + - ./file.rb + YAML + @project.write!("file.rb", <<~RB) + # typed: false + class A; end + RB + + result = @project.spoom("srb bump --no-color") + assert_empty(result.out) + assert_equal(<<~ERR, result.err) + Error: Cannot run `spoom bump` on a project that already uses Sorbet's `--typed-override` option + ERR + refute(result.status) + + assert_equal("false", @project.read_file_strictness("file.rb")) + end + def test_bump_doesnt_change_sigils_outside_directory @project.write!("lib/a/file.rb", "# typed: false") @project.write!("lib/b/file.rb", "# typed: false") diff --git a/test/spoom/sorbet/config_test.rb b/test/spoom/sorbet/config_test.rb index 8aac162c..717fe213 100644 --- a/test/spoom/sorbet/config_test.rb +++ b/test/spoom/sorbet/config_test.rb @@ -71,6 +71,19 @@ def test_parses_a_config_string_with_ignore_options assert_empty(config.allowed_extensions) end + def test_parses_a_config_string_with_typed_override_options + config = Spoom::Sorbet::Config.parse_string(<<~CONFIG) + a + --typed-override=b.yaml + c + --typed-override + d.yaml + e + CONFIG + assert_equal(["a", "c", "e"], config.paths) + assert_equal(["b.yaml", "d.yaml"], config.typed_overrides) + end + def test_parses_a_config_string_with_other_options config = Spoom::Sorbet::Config.parse_string(<<~CONFIG) a @@ -241,12 +254,14 @@ def test_options_string_with_options --ignore=.git/ --ignore=vendor/ --allowed-extension=.rb + --typed-override=typed-override.yaml --no-stdlib CONFIG expected = "'.' " \ "--ignore '.git/' " \ "--ignore 'vendor/' " \ "--allowed-extension '.rb' " \ + "--typed-override 'typed-override.yaml' " \ "--no-stdlib" assert_equal(expected, config.options_string) end