Enforce Sorbet typed sigil - #140
Conversation
Co-authored-by: JPDuchesne <2636122+JPDuchesne@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
|
||
| # Ensure the wrapper function + completer are in the user's shell RC. | ||
| # | ||
| # @return [Symbol, false] :added, :already_present, or false (unsupported shell) |
There was a problem hiding this comment.
This typing is actually pretty bad, we should likely model this as what the name of this method actually means which simplifies the return type:
- ensure the hook is installed, idempotent. an idempotent method caller has no care for whether it was already done or not, thus get rid of :already_present, which leaves :added as a success case or false for unsupported shell. ensuring the hook is installed it the success case, so not raising is the success case. raising for UnsupportedShellError or something (likely caught at a higher level)
Let's create a GH issue to refactor
There was a problem hiding this comment.
Created issue #144 for the refactor: idempotent ensure semantics, drop :already_present, success = not raising, typed UnsupportedShellError caught at a higher level — scoped to ShellRcHook, HookInstaller, and the three callers that branch on the tri-state.
Addressed in 5a3024a.
| @@ -1,3 +1,4 @@ | |||
| # typed: false | |||
There was a problem hiding this comment.
dsl.rb's method_missing dispatch into a required-keyword method needs T.unsafe to typecheck, and T.unsafe is sorbet-runtime — forbidden in the pre-bundle chain — so it stays typed: false; documented exclude.
Addressed in 5a3024a.
There was a problem hiding this comment.
dsl.rb is typed: false because its method_missing dispatch into required-keyword methods needs T.unsafe, which the pre-bundle no-sorbet-runtime constraint forbids; documented in the exclude comments.
Addressed in d195e32.
| @@ -1,3 +1,4 @@ | |||
| # typed: false | |||
There was a problem hiding this comment.
fetcher.rb consumes a consumer-repo Lockfile API (parse, runtime_ref_map) that doesn't resolve against this repo's Lockfile class, so type checking it produces false errors; typed: false with a documented exclude, also pre-bundle.
Addressed in 5a3024a.
There was a problem hiding this comment.
fetcher.rb is typed: false because it consumes a consumer-repo Lockfile API (parse, runtime_ref_map) that doesn't resolve against this repo's Lockfile; documented in the exclude comments.
Addressed in d195e32.
| params( | ||
| targets: T.nilable(T::Array[T::Hash[String, T.untyped]]), | ||
| target_name: String, | ||
| ).returns(T.nilable(T::Hash[String, T.untyped])) | ||
| end | ||
| def find_target(targets, target_name) | ||
| return nil if targets.nil? || targets.empty? |
There was a problem hiding this comment.
why is targets even nilable if the method returns nil when targets is nil ? we should remove the representation of a nilable targets here
There was a problem hiding this comment.
Removed the nilable representation: find_target now takes T::Array[Hash] and the one caller owns the API-boundary coercion (version_data["targets"] || []); the return stays nilable only for the genuinely-empty case, and a test covers the transport method the change touched.
Addressed in 5a3024a.
There was a problem hiding this comment.
Already fixed on the branch: find_target now takes a non-nilable T::Array and the caller passes version_data["targets"] || [] — the nilable representation is gone.
Addressed in d195e32.
| sig { params(body: T::Hash[Symbol, T.untyped]).returns(Net::HTTPResponse) } | ||
| def post_graphql(body) | ||
| http = Net::HTTP.new(GRAPHQL_ENDPOINT.host, GRAPHQL_ENDPOINT.port) | ||
| http = Net::HTTP.new(T.must(GRAPHQL_ENDPOINT.host), GRAPHQL_ENDPOINT.port) |
There was a problem hiding this comment.
/ask Fine I guess, but is there a reason we do it like this rather than capturing the host as a constant too ? or better yet is there a better way to do this from the URI directly? I would imagine there's some interoperability between these modules?
There was a problem hiding this comment.
✅ /ask
Why the T.must: Sorbet's stdlib payload types URI(...) as URI::Generic, whose #host is T.nilable(String) — host-less URIs (relative refs, mailto:) are representable, and Sorbet can't see that this particular literal always parses with a host. Verified against this repo's Sorbet: T.reveal_type(URI("https://x.dev/q").host) → T.nilable(String). So the narrow has to happen somewhere; the only question is where.
Capturing the host as a constant — yes, strictly better than the per-call T.must: GRAPHQL_HOST = T.let(T.must(GRAPHQL_ENDPOINT.host), String) next to the endpoint constant does the narrow once at load (fail-fast if the URL is ever edited into something host-less) and keeps call sites clean.
URI-direct interop — exists, and it's the nicer fix: Net::HTTP.post(GRAPHQL_ENDPOINT, JSON.generate(body), "Content-Type" => "application/json") (Ruby ≥ 2.4) takes the URI whole, derives use_ssl from the scheme, and deletes the entire new/use_ssl/Net::HTTP::Post dance — the response-code check stays. One caveat, also verified: the payload leaves Net::HTTP.post's return untyped, so under strict the result wants a T.let(..., Net::HTTPResponse) — one escape hatch traded for another, but ~5 fewer lines and no per-part URI plucking. Either variant is an improvement over the inline T.must; the Net::HTTP.post form is a small follow-up candidate (this method is untouched by the deps redesign in #142, so it can land on either branch).
There was a problem hiding this comment.
let's record this in a GH issue
| ) do | ||
| extend T::Sig | ||
|
|
||
| # Sorbet's Data.define rewriter can't attach sigs to the generated |
There was a problem hiding this comment.
/ask there has to be a cleaner way to support Data, no? (i.e. tapioca or rbis?) otherwise I guess we use a class rather than Data.define ?
There was a problem hiding this comment.
✅ /ask
No clean way exists today — the wall is sorbet/sorbet#7272 (cited in the code comment): Sorbet's Data.define rewriter synthesizes the member readers without sigs, strict demands a sig on every method, and re-opening the block to define sigged readers with the same names would replace the C-level originals. The escape routes you'd hope for don't apply:
- tapioca: generates RBIs for gems (
tapioca gem) and framework metaprogramming via DSL compilers (tapioca dsl) — there's no compiler for app-localData.define, and tapioca doesn't process your ownlib/at all. - hand-written RBI shims:
sorbet/rbi/shimsis for methods Sorbet can't see. These readers ARE seen (the rewriter defines them), so a shim is a conflicting duplicate definition rather than a sig attachment — and it drifts silently when members change.
Hence the to_h-delegating explicit readers here: ugly, but honest about the constraint.
So yes — the pragmatic conclusion is the one you guessed: a class rather than Data.define. That's already the precedent on the stacked branch: #142's new domain value types (PackageId, PackageVersion, Artifact, …) are plain classes with attr_reader + sigs, because Data.define additionally hits error 4010 there once initialize carries keyword defaults.
For Entry specifically, T::Struct is the zero-boilerplate fit: registry.rb is not in the pre-bundle chain, so sorbet-runtime is available; const :symbol, Symbol (etc.) gives typed readers and keyword construction with defaults for free; and Entry uses none of what Data buys (value equality, #with) — it's config rows, not a value type. That conversion would delete this whole reader block. Best done as a follow-up on #142's branch, since this file is heavily reshaped there (scheme/locker slots) and doing it here would just manufacture a conflict.
|
/build let's swipe the current PR review feedback + fix codecov
|
|
Two of this review's three |
Co-authored-by: JPDuchesne <2636122+JPDuchesne@users.noreply.github.com>
| # Required here (not just in src/dev.rb) because consumer dependencies.rb | ||
| # files load this hook standalone via bin/install-build-deps.rb, where the | ||
| # Dev module root never loads. | ||
| require "sorbet-runtime" |
There was a problem hiding this comment.
is there a more central place we should put this require?
There was a problem hiding this comment.
The central place now exists — src/dev.rb carries the one sorbet-runtime require for the whole Dev tree — but the hooks are the deliberate exception: consumer dependencies.rb files load them standalone via bin/install-build-deps.rb, where the Dev root never loads, so each hook keeps its own require with a comment saying exactly that.
Addressed in d195e32.
| # Required here (not just in src/dev.rb) because consumer dependencies.rb | ||
| # files load this hook standalone via bin/install-build-deps.rb, where the | ||
| # Dev module root never loads. | ||
| require "sorbet-runtime" |
There was a problem hiding this comment.
same, is there a more central place we should put this require?
There was a problem hiding this comment.
Same as thread 13 — wwise_download.rb is loaded standalone by consumer repos, so the self-require stays, documented in place.
Addressed in d195e32.
| sig { params(body: T::Hash[Symbol, T.untyped]).returns(Net::HTTPResponse) } | ||
| def post_graphql(body) | ||
| http = Net::HTTP.new(GRAPHQL_ENDPOINT.host, GRAPHQL_ENDPOINT.port) | ||
| http = Net::HTTP.new(T.must(GRAPHQL_ENDPOINT.host), GRAPHQL_ENDPOINT.port) |
There was a problem hiding this comment.
let's record this in a GH issue
| @@ -1,3 +1,4 @@ | |||
| # typed: true | |||
There was a problem hiding this comment.
typed: true? why not strict?
There was a problem hiding this comment.
Pre-bundle chain: lockfile.rb is loaded by bin/setup.rb before the bundle exists, so typed: true is its ceiling (documented holdout).
Addressed in d195e32.
| ) do | ||
| extend T::Sig | ||
|
|
||
| # Sorbet's Data.define rewriter can't attach sigs to the generated |
|
|
||
| require "fileutils" | ||
| require "pathname" | ||
| require "sorbet-runtime" |
There was a problem hiding this comment.
Same thing here with regards to namespacing and sorbet-runtime require
There was a problem hiding this comment.
Done: Dev::ShadowenvPython, require dropped.
Addressed in d195e32.
|
|
||
| require "fileutils" | ||
| require "pathname" | ||
| require "sorbet-runtime" |
There was a problem hiding this comment.
Same thing here with regards to namespacing and sorbet-runtime require
There was a problem hiding this comment.
Done: Dev::ShadowenvRuby, require dropped.
Addressed in d195e32.
|
|
||
| require "fileutils" | ||
| require "pathname" | ||
| require "sorbet-runtime" |
There was a problem hiding this comment.
Same thing here with regards to namespacing and sorbet-runtime require
There was a problem hiding this comment.
Done: Dev::ShadowenvUnreal, require dropped.
Addressed in d195e32.
|
|
||
| require "fileutils" | ||
| require "pathname" | ||
| require "sorbet-runtime" |
There was a problem hiding this comment.
Same thing here with regards to namespacing and sorbet-runtime require
There was a problem hiding this comment.
Done: Dev::ShadowenvXcode, require dropped.
Addressed in d195e32.
| $stdin.stubs(:tty?).returns(true) | ||
| $stdin.stubs(:noecho).returns("") | ||
| $stdout.stubs(:puts) | ||
| $stdout.stubs(:print) |
There was a problem hiding this comment.
we should be able to use RSpock interaction-based stubbing here
There was a problem hiding this comment.
Converted to RSpock interaction stubbing in test/dev/credentials_test.rb: _ * $stdin.tty? >> true, _ * $stdin.noecho >> "", and the $stdout silencers are now declared in the Then block (installed before When runs), replacing the mocha .stubs calls.
Addressed in d195e32.
|
/build let's swipe the review feedback
|
Co-authored-by: JPDuchesne <2636122+JPDuchesne@users.noreply.github.com>
…aded The codecov-action auto-search was picking up coverage/coverage.json (SimpleCov native JSON with skipped lines from simplecov-sorbet) alongside the pinned cobertura XML, causing Codecov to process every commit to an empty error report and never post statuses. Co-authored-by: Cursor <cursoragent@cursor.com>
Covers setup!, the Homebrew binary-fallback version detection, brew_prefix_for / path_with_brew_bin / homebrew_prefix, the fresh-install and not-found paths of ensure_ruby_installed!, ensure_ruby_build_deps!, the single-part gem_api_version fallback, and the shell-hook write-failure rescue - all via fake executables and unwritable real directories. Co-authored-by: Cursor <cursoragent@cursor.com>
Covers setup!, venv creation/reuse/failure in ensure_venv!, the ensurepip bootstrap and its failure in ensure_pip!, every outcome of ensure_homebrew_python! (installed, install, install-failure, prefix unresolvable, python3 fallback, no interpreter), and brew_prefix_for - all via fake brew/python executables on a restricted PATH. Co-authored-by: Cursor <cursoragent@cursor.com>
Covers wait_or_kill killing a silent idle build, container_cpu parsing the docker stats percentage, and the no-docker-executable idle fallback, all via fake docker executables on PATH. Co-authored-by: Cursor <cursoragent@cursor.com>
Covers prewarm_container_name, the system-delegating wrappers (remove_image, container_exists?, start_container, remove_container, local_image?, pull, push!, registry_has?), the docker ps name parsing in service_containers, container_running?, and run_watched's watcher delegation. Co-authored-by: Cursor <cursoragent@cursor.com>
Covers detect_llvm_prefix finding an installed formula's keg, the no-usable-keg nil path, and the brew-absent nil path, all via fake brew executables on PATH. Co-authored-by: Cursor <cursoragent@cursor.com>
A fake brew reports lua@5.1 installed but fails the luarocks install, so ensure_homebrew_lua! raises BrewInstallError naming luarocks. Co-authored-by: Cursor <cursoragent@cursor.com>
platform_subdir branched on the RUBY_PLATFORM constant, so any single CI platform could only ever cover its own branch. It now takes the platform as a defaulted parameter, and a Where-table exercises the darwin, linux, and windows mappings. Co-authored-by: Cursor <cursoragent@cursor.com>
Require blocks keep the redesign's package/package_id/package_version requires and adopt main's convention of loading sorbet-runtime once in src/dev.rb (applied to every new deps file, matching the sweep in 5a3024a). Ficsit's find keeps the redesign body with main's single target binding ported. Dead main-side requires (tempfile, digest/open3/ tmpdir, repository.rb's dependency pair) dropped with the code that used them. Co-authored-by: Cursor <cursoragent@cursor.com>
Implements #139.
Requested by @JPDuchesne.
Closes #139