Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 64 additions & 2 deletions lib/tapioca/helpers/sorbet_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,81 @@ module SorbetHelper

SPOOM_CONTEXT = Spoom::Context.new(".") #: Spoom::Context

# Represents the `sorbet/config` file, and provides access to its options. https://sorbet.org/docs/cli-ref
# If the file doesn't exist, this object will still exist, but will return default values for all options.
class SorbetConfig
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow, Claude just spit this out so easily, didn't even realize this already exists: https://github.com/Shopify/spoom/blob/main/lib/spoom/sorbet/config.rb

class << self
#: (Spoom::Context spoom_context) -> SorbetConfig
def parse_from(spoom_context)
config_path = File.join(spoom_context.absolute_path, "sorbet", "config")
content = File.exist?(config_path) ? File.read(config_path) : ""
parse(content)
end

#: (String content) -> SorbetConfig
def parse(content)
lines = content.lines.map(&:strip).reject(&:empty?)

options = lines.filter_map do |line|
next if line.start_with?("#") # Skip comments
next unless line.start_with?("--")

key, value = line.split("=", 2)
key = key #: as !nil

[key, value]
end.to_h #: Hash[String, String | bool | nil]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we get bool?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


new(
parser: options["--parser"] == "prism" ? :prism : :original,
)
end
end

#: (parser: Symbol) -> void
def initialize(parser:)
@parser = parser #: Symbol
end

#: Symbol
attr_reader :parser

#: -> bool
def parse_with_prism? = @parser == :prism
end

FEATURE_REQUIREMENTS = {
# feature_name: ::Gem::Requirement.new(">= ___"), # https://github.com/sorbet/sorbet/pull/___

prism_syntax_check_with_stop_after_parser: ::Gem::Requirement.new("> 0.6.13073"), # https://github.com/sorbet/sorbet/pull/10076
}.freeze #: Hash[Symbol, ::Gem::Requirement]

#: (*String sorbet_args) -> Spoom::ExecResult
def sorbet(*sorbet_args)
if sorbet_config.parse_with_prism?
sorbet_args << "--parser=prism"
end

SPOOM_CONTEXT.srb(sorbet_args.join(" "), sorbet_bin: sorbet_path)
end

#: (String, rbi_mode: bool) { (String stderr) -> void } -> void
def sorbet_syntax_check!(source, rbi_mode:, &on_failure)
quoted_source = "\"#{source}\""

stop_after = "--stop-after=parser"

# This version of Sorbet doesn't report parse errors until the desugarer, so we need to modify the
# stop-after argument to get far enough to get those errors (and a non-zero exit code).
if sorbet_config.parse_with_prism? && !sorbet_supports?(:prism_syntax_check_with_stop_after_parser)
stop_after = "--stop-after=desugarer"
end

result = if rbi_mode
# --e-rbi cannot be used on its own, so we pass a dummy value like `-e ""`
sorbet("--no-config", "--stop-after=parser", "-e", '""', "--e-rbi", quoted_source)
sorbet("--no-config", stop_after, "-e", '""', "--e-rbi", quoted_source)
else
sorbet("--no-config", "--stop-after=parser", "-e", quoted_source)
sorbet("--no-config", stop_after, "-e", quoted_source)
end

unless result.status
Expand All @@ -41,6 +98,11 @@ def sorbet_syntax_check!(source, rbi_mode:, &on_failure)
nil
end

#: -> SorbetConfig
def sorbet_config
@sorbet_config ||= SorbetConfig.parse_from(SPOOM_CONTEXT) #: SorbetConfig?
end

#: -> String
def sorbet_path
sorbet_path = ENV.fetch(SORBET_EXE_PATH_ENV_VAR, SORBET_BIN)
Expand Down
7 changes: 4 additions & 3 deletions spec/tapioca/dsl/compiler_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,10 @@ def foo(d:, e: 42, **f, &blk); end
end
RUBY

assert_raises(SyntaxError, /void\)/) do
rbi_for(:Post)
end
e = assert_raises(SyntaxError) { rbi_for(:Post) }

assert_match("unexpected ')'", e.message)
assert_match("sig { returns(void)) }", e.message)
end
end
end
Expand Down
6 changes: 3 additions & 3 deletions spec/tapioca/dsl/compilers/json_api_client_resource_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,9 @@ class Post < JsonApiClient::Resource
# We let the generation raise, the user should not define an association without a corresponding class.
# The association will be unusable anyway.

assert_raises(NameError, /uninitialized constant Post::User/) do
rbi_for(:Post)
end
e = assert_raises(NameError) { rbi_for(:Post) }

assert_match("uninitialized constant Post::User", e.message)
end

it "generates associations" do
Expand Down
69 changes: 68 additions & 1 deletion spec/tapioca/helpers/sorbet_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,74 @@ class Tapioca::SorbetHelperSpec < Minitest::Spec
end
end

private
describe Tapioca::SorbetHelper::SorbetConfig do
it "ignores comment lines" do
config = parse(<<~CONFIG)
# --parser=prism
CONFIG
assert_equal(:original, config.parser)
end

it "ignores blank lines" do
config = parse(<<~CONFIG)

--parser=prism

CONFIG
assert_equal(:prism, config.parser)
end

it "ignores lines without -- prefix" do
config = parse(<<~CONFIG)
.
src/
--parser=prism
CONFIG
assert_equal(:prism, config.parser)
end

describe "--parser" do
it "detects --parser=prism" do
config = parse(<<~CONFIG)
.
--parser=prism
CONFIG
assert_equal(:prism, config.parser)
assert_predicate(config, :parse_with_prism?)
end

it "defaults to :original for empty config" do
config = parse("")
assert_equal(:original, config.parser)
refute_predicate(config, :parse_with_prism?)
end

it "defaults to :original when no --parser option" do
config = parse(<<~CONFIG)
.
--dir=foo
CONFIG
assert_equal(:original, config.parser)
end

it "treats non-prism values as :original" do
config = parse(<<~CONFIG)
.
--parser=original
CONFIG
assert_equal(:original, config.parser)
refute_predicate(config, :parse_with_prism?)
end
end

private

#: (String content) -> Tapioca::SorbetHelper::SorbetConfig
def parse(content) = Tapioca::SorbetHelper::SorbetConfig.parse(content)
end

# Rubocop thinks the `private` call above (in the `describe` block) still applies here. It doesn't.
private # rubocop:disable Lint/UselessAccessModifier

#: (String? path) { (String? custom_path) -> void } -> void
def with_custom_sorbet_exe_path(path, &block)
Expand Down
Loading