# typed: strict # frozen_string_literal: true module Spoom class Context # Sorbet features for a context module Sorbet extend T::Sig extend T::Helpers requires_ancestor { Context } # Run `bundle exec srb` in this context directory sig { params(arg: String, sorbet_bin: T.nilable(String), capture_err: T::Boolean).returns(ExecResult) } def srb(*arg, sorbet_bin: nil, capture_err: true) res = if sorbet_bin exec("#{sorbet_bin} #{arg.join(" ")}", capture_err: capture_err) else bundle_exec("srb #{arg.join(" ")}", capture_err: capture_err) end case res.exit_code when Spoom::Sorbet::KILLED_CODE raise Spoom::Sorbet::Error::Killed.new("Sorbet was killed.", res) when Spoom::Sorbet::SEGFAULT_CODE raise Spoom::Sorbet::Error::Segfault.new("Sorbet segfaulted.", res) end res end sig { params(arg: String, sorbet_bin: T.nilable(String), capture_err: T::Boolean).returns(ExecResult) } def srb_tc(*arg, sorbet_bin: nil, capture_err: true) arg.prepend("tc") unless sorbet_bin T.unsafe(self).srb(*arg, sorbet_bin: sorbet_bin, capture_err: capture_err) end sig do params( arg: String, sorbet_bin: T.nilable(String), capture_err: T::Boolean, ).returns(T.nilable(T::Hash[String, Integer])) end def srb_metrics(*arg, sorbet_bin: nil, capture_err: true) metrics_file = "metrics.tmp" T.unsafe(self).srb_tc( "--metrics-file", metrics_file, *arg, sorbet_bin: sorbet_bin, capture_err: capture_err, ) return nil unless file?(metrics_file) metrics_path = absolute_path_to(metrics_file) metrics = Spoom::Sorbet::MetricsParser.parse_file(metrics_path) remove!(metrics_file) metrics end # List all files typechecked by Sorbet from its `config` sig { params(with_config: T.nilable(Spoom::Sorbet::Config), include_rbis: T::Boolean).returns(T::Array[String]) } def srb_files(with_config: nil, include_rbis: true) config = with_config || sorbet_config allowed_extensions = config.allowed_extensions allowed_extensions = Spoom::Sorbet::Config::DEFAULT_ALLOWED_EXTENSIONS if allowed_extensions.empty? allowed_extensions -= [".rbi"] unless include_rbis excluded_patterns = config.ignore.map do |string| # We need to simulate the behavior of Sorbet's `--ignore` flag. # # From Sorbet docs on `--ignore`: # > Ignores input files that contain the given string in their paths (relative to the input path passed to # > Sorbet). Strings beginning with / match against the prefix of these relative paths; others are substring # > matchs. Matches must be against whole folder and file names, so `foo` matches `/foo/bar.rb` and # > `/bar/foo/baz.rb` but not `/foo.rb` or `/foo2/bar.rb`. string = if string.start_with?("/") # Strings beginning with / match against the prefix of these relative paths File.join(absolute_path, string) else # Others are substring matchs File.join(absolute_path, "**", string) end # Matches must be against whole folder and file names "#{string.delete_suffix("/")}{,/**}" end collector = FileCollector.new(allow_extensions: allowed_extensions, exclude_patterns: excluded_patterns) collector.visit_paths(config.paths.map { |path| absolute_path_to(path) }) collector.files.map { |file| file.delete_prefix("#{absolute_path}/") }.sort end # List all files typechecked by Sorbet from its `config` that matches `strictness` sig do params( strictness: String, with_config: T.nilable(Spoom::Sorbet::Config), include_rbis: T::Boolean, ).returns(T::Array[String]) end def srb_files_with_strictness(strictness, with_config: nil, include_rbis: true) srb_files(with_config: with_config, include_rbis: include_rbis) .select { |file| read_file_strictness(file) == strictness } end sig { params(arg: String, sorbet_bin: T.nilable(String), capture_err: T::Boolean).returns(T.nilable(String)) } def srb_version(*arg, sorbet_bin: nil, capture_err: true) res = T.unsafe(self).srb_tc("--no-config", "--version", *arg, sorbet_bin: sorbet_bin, capture_err: capture_err) return nil unless res.status res.out.split(" ")[2] end # Does this context has a `sorbet/config` file? sig { returns(T::Boolean) } def has_sorbet_config? file?(Spoom::Sorbet::CONFIG_PATH) end sig { returns(Spoom::Sorbet::Config) } def sorbet_config Spoom::Sorbet::Config.parse_string(read_sorbet_config) end # Read the contents of `sorbet/config` in this context directory sig { returns(String) } def read_sorbet_config read(Spoom::Sorbet::CONFIG_PATH) end # Set the `contents` of `sorbet/config` in this context directory sig { params(contents: String, append: T::Boolean).void } def write_sorbet_config!(contents, append: false) write!(Spoom::Sorbet::CONFIG_PATH, contents, append: append) end # Read the strictness sigil from the file at `relative_path` (returns `nil` if no sigil) sig { params(relative_path: String).returns(T.nilable(String)) } def read_file_strictness(relative_path) Spoom::Sorbet::Sigils.file_strictness(absolute_path_to(relative_path)) end # Get the commit introducing the `sorbet/config` file sig { returns(T.nilable(Spoom::Git::Commit)) } def sorbet_intro_commit res = git_log("--diff-filter=A --format='%h %at' -1 -- sorbet/config") return nil unless res.status out = res.out.strip return nil if out.empty? Spoom::Git::Commit.parse_line(out) end # Get the commit removing the `sorbet/config` file sig { returns(T.nilable(Spoom::Git::Commit)) } def sorbet_removal_commit res = git_log("--diff-filter=D --format='%h %at' -1 -- sorbet/config") return nil unless res.status out = res.out.strip return nil if out.empty? Spoom::Git::Commit.parse_line(out) end end end end