Skip to content

Give task_to_h a per-key return type - #479

Draft
apiology wants to merge 7 commits into
mainfrom
type-task-to-h-per-key
Draft

apiology wants to merge 7 commits into
mainfrom
type-task-to-h-per-key

Conversation

@apiology

Copy link
Copy Markdown
Owner

Problem:

Checkoff::Tasks#task_to_h and Checkoff::Internal::TaskHashes#task_to_h both declared @return [Hash], which resolves to RBS Hash[untyped, untyped]. That leaves Hash#fetch's generic type variables unresolved for every caller — the same gap project_to_h had before #478 gave it a precise per-key intersection type.

task_data = task_hashes.task_to_h(task)
task_data.fetch('unwrapped').fetch('custom_fields').fetch('Estimated time').fetch('number_value')
# strong typecheck: Declared return type ::Integer does not match inferred
# type ::Asana::Resources::Task, ::Integer, generic<T>, generic<U>, 0

Solution:

Apply the same pattern #478 used for project_to_h: an intersection of single-key Hash record types covering the keys this repo's own callers actually fetch (gid, name, task, memberships, custom_fields, unwrapped's nested custom_fields, and the four membership_by_* groupings). Checkoff::Tasks#task_to_h's own doc now points at Checkoff::Internal::TaskHashes#task_to_h's (@return (see ...)) instead of duplicating it.

Three call sites in selector_classes/task.rb had local @type comments narrowing task_data/unwrapped to a looser shape than the method now actually returns; removed, since the more precise inferred type covers them without narrowing.

Claude: opened on behalf of the repo owner while auditing @sg-ignore suppressions in a downstream project.

The apiology/solargraph 2026-08-04 branch moved past the revision
this worktree's checked-out lockfile pinned, so bundle install
failed with Bundler::GemNotFound. Re-lock against the branch's
current tip, refresh the matching rbs collection, and regenerate
the gem RBIs make build-typecheck produces from that (rbs, rubocop
and yard all picked up patch bumps in the process).
Checkoff::Tasks#task_to_h and Checkoff::Internal::TaskHashes#task_to_h
both declared @return [Hash], which resolves to RBS Hash[untyped,
untyped]. That leaves Hash#fetch's generic type variables unresolved
for every caller, matching the gap project_to_h had before PR 478
gave it a precise per-key intersection type.

Apply the same pattern here: an intersection of single-key Hash
record types covering the keys this repo's own callers actually
fetch (gid, name, task, memberships, custom_fields, unwrapped's
nested custom_fields and four membership_by_* groupings).

Three call sites in selector_classes/task.rb had local @type
comments narrowing task_data/unwrapped to a looser shape than the
method now actually returns; removed them so the more precise
inferred type flows through instead of conflicting with it.

Verified: solargraph typecheck --level strong is clean project-wide
(the 9 remaining findings are all pre-existing, in files this commit
doesn't touch). rubocop clean except for one pre-existing,
untouched-line offense at selector_classes/task.rb:160
(Style/DirectiveScope), unrelated to this change.
test/unit/internal/test_task_hashes.rb, test_tasks.rb,
test_task_selectors.rb and test_timelines.rb all pass.
Bundle was locked to 9bf846d on apiology/solargraph's 2026-08-04
branch, which no longer exists at that revision. Update to the
current branch tip, 0b97e98.
Two @sg-ignore comments (attachments.rb, punchlist.rb) are unneeded
against the current solargraph pin -- verified by removing and
re-typechecking clean. task_options gets a per-key intersection
Hash type (matching task_to_h's own pattern) and a project_gid
keyword instead of mutating its result, clearing the remaining
per-key inference conflict.
Pin a T.must annotation (ported from plate-spinner's
config/annotations_sorbet.rb) so Solargraph narrows the generic
rather than leaving the result untyped -- this resolved two
regex-capture nilability findings whose groups are structurally
mandatory (no optional/alternation), safe to assert non-nil.

selector_classes/task.rb's last_story.created_at is genuinely
nilable per the Asana SDK's own signature, unlike the regex
captures above -- extracted a created_at_for helper with a real
guard and a descriptive error instead of T.must, since bad API
data (not local code) could produce nil there; also keeps
evaluate's own AbcSize under the RuboCop ratchet, and fixes a
pre-existing disable/enable-pair RuboCop offense on an unrelated
method in the same file.

test_asana_event_enrichment.rb: use #fetch instead of #[] to get a
non-nilable Hash value type.

test_tags.rb: task_params gets a per-key intersection Hash type,
same pattern already used for the campaign's task_to_h work.

test_date_param_converter.rb: mark the deliberate invalid-input
test case with @sg-ignore, rather than changing the negative test
it verifies a runtime guard against.
Undercover flagged the new guard branch as untested. Also fixes a
pre-existing Style/DirectiveScope offense on this file's own
class-length disable/enable pair while RuboCop was already
touching the file (disable-next covers the whole class as one
statement, same as the earlier single-method case).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant