Skip to content

Order-dependent: real method def after typed_let_mock/@!macro call corrupts that call's type resolutionΒ #1233

Description

@apiology

πŸ€– Filed by Claude, not the account owner β€” acting on their behalf via their GitHub credentials.

Follow-up to #1229 / the in-progress fix in #1231 (branch fix-1229-intersection-types). While rolling out A & B intersection types across apiology/checkoff's test suite (via a custom typed_let_mock/typed_mock @!macro that expands to @!method $1 @return [Mocha::Mock & $2]), I found a reliable, order-dependent bug: a real method definition placed after a macro-expanding call (e.g. typed_let_mock) in the same class body corrupts type resolution for that call. Moving the macro call above the method definition fixes it every time.

Two manifestations observed

  1. False positive: multiple stacked @!parse-based type overrides for def_delegators-generated methods in one class β€” whichever override is declared last fails with Declared return type X does not match inferred type nil for ClassName#method_name, regardless of grouping (one comment block vs. several) or which symbol happens to be last.
  2. Silent revert to untyped: a typed_let_mock :task, Asana::Resources::Task call placed after a real method definition (with two unrelated let_mock lines in between) silently reverts to plain Mocha::Mock β€” no error at that call, but every downstream call site using the mock then fails as if the intersection type was never applied.

Reproduction (manifestation #2, from the real checkoff project)

test/unit/test_tasks.rb, minimal diff that introduces the bug (moving typed_let_mock :task from before the client method to after it, past two intervening let_mock lines):

diff --git a/test/unit/test_tasks.rb b/test/unit/test_tasks.rb
index 4a96805..7e3c7ac 100644
--- a/test/unit/test_tasks.rb
+++ b/test/unit/test_tasks.rb
@@ -16,8 +16,6 @@ class TestTasks < BaseAsana
   def_delegators :@mocks, :sections, :asana_task, :time_class, :date_class, :workspaces,
                  :portfolios
 
-  typed_let_mock :task, Asana::Resources::Task
-
   # rubocop:disable YARD/TagTypeSyntax
   # @return [Mocha::Mock & Asana::Client]
   # @sg-ignore TestTasks#client return type could not be inferred
@@ -33,6 +31,8 @@ def client
 
   let_mock :default_workspace, :workspace_gid, :task_name, :default_assignee_gid
 
+  typed_let_mock :task, Asana::Resources::Task
+
   let_mock :start_on_string, :start_on_date_obj,
            :start_on_time_obj,
            :start_at_string, :start_at_time_obj,

Where typed_let_mock is defined via:

# @!macro [attach] typed_let_mock
#   @!method $1
#     @return [Mocha::Mock & $2]

and client is a real accessor method:

# rubocop:disable YARD/TagTypeSyntax
# @return [Mocha::Mock & Asana::Client]
# @sg-ignore TestTasks#client return type could not be inferred
def client
  # @sg-ignore Unresolved call to client on MyOpenStruct
  mocks.client
end
# rubocop:enable YARD/TagTypeSyntax

With the diff applied (typed_let_mock :task after client), bundle exec solargraph typecheck test/unit/test_tasks.rb --level strong --directory . reports 18 problems, all of the form:

Wrong argument type for Checkoff::Tasks#<method>: task expected Asana::Resources::Task, received Mocha::Mock

at 18 distinct call sites (lines 92, 110, 128, 154, 171, 219, 232, 258, 311, 436, 454, 502, 525, 543, 562, 573, 590, 607) β€” every use of the task helper in the file, as if typed_let_mock :task, Asana::Resources::Task had never been called at all.

Reverting the diff (moving typed_let_mock :task back above client, no other changes) restores a clean 0 problems found.

Notes

  • This is specific to the fix-1229-intersection-types branch (Add intersection (A & B), union (|), and grouping ([...]) type syntaxΒ #1231) β€” I haven't checked whether it reproduces on master with plain (non-intersection) @!method/@!macro overrides, since the checkoff test suite's use of this pattern only exists because of the intersection-type work in Add intersection (A & B), union (|), and grouping ([...]) type syntaxΒ #1231.
  • I could not get a minimal reproduction to work in an isolated scratch directory (Unresolved call to typed_let_mock β€” the macro itself failed to register outside the real project's bundle/workspace context), so the reproduction above is against the real apiology/checkoff project. Happy to provide more of that project's Solargraph config (config/annotations_misc.rb, .solargraph.yml) if useful.
  • Workaround in use: always declare macro-expanding calls (typed_let_mock, typed_mock) before any real method definitions in the same class body.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions