Skip to content

Index X = Class.new(Super) constant assignments as classes - #1306

Open
apiology wants to merge 1 commit into
castwide:masterfrom
apiology:fix-1303-class-new-constant
Open

Index X = Class.new(Super) constant assignments as classes#1306
apiology wants to merge 1 commit into
castwide:masterfrom
apiology:fix-1303-class-new-constant

Conversation

@apiology

Copy link
Copy Markdown
Contributor

Fixes #1303.

Gems commonly build classes as X = Class.new(Parent) do ... end — Asana's entire error hierarchy is one example. Solargraph indexed the result as an untyped constant, so no method on such a class resolved, and the methods in the block were attached to the enclosing namespace instead of the new class. There was no annotation that worked around it: a @!parse stub declaring the same name contributed nothing, and @!override on the constant crashed the typechecker (#1302).

module Vendor
  Specific = Class.new(StandardError) do
    # @return [Integer]
    def retry_after
      5
    end
  end
end

# @param e [Vendor::Specific]
# @return [void]
def wait_for(e)
  e.retry_after   # Unresolved call to retry_after on Vendor::Specific
end

This adds a Convention::ClassDefinition following the existing StructDefinition / DataDefinition pattern: a :casgn node processor that pushes a Pin::Namespace for the assigned constant plus a Pin::Reference::Superclass, so block methods attach to the new class and the superclass is recorded. It registers after the Struct/Data processors and before ParserGem::NodeProcessors::CasgnNode, since registration order is precedence and Struct/Data must keep winning.

Negative controls verified in one file covering all four forms: S = Struct.new(:bar) and D = Data.define(:baz) still resolve through their own conventions, a plain Alias = Other is still an ordinary Pin::Constant, and C = Class.new(StandardError) do … end now resolves its block methods. Full suite green locally: 1632 examples, 0 failures, 60 pending. RuboCop clean; strong typecheck on both new lib files reports 0 problems.

Authored by Claude (Anthropic's Claude Code) on behalf of @apiology.

Gems commonly build classes with an anonymous-class assignment instead of
a `class` keyword, e.g. Asana's error hierarchy:

    module Vendor
      Specific = Class.new(StandardError) do
        # @return [Integer]
        def retry_after
          5
        end
      end
    end

`ParserGem::NodeProcessors::CasgnNode` mapped that `casgn` to an untyped
`Pin::Constant`, and the methods in the block landed on the enclosing
namespace, so `Vendor::Specific#retry_after` did not resolve:
`Unresolved call to retry_after on Vendor::Specific` at level strong.

Adds `Convention::ClassDefinition`, following the existing
`Convention::StructDefinition` / `Convention::DataDefinition` pattern: a
`casgn` node processor that recognizes `Class.new(...)` (with or without
a block), pushes a `Pin::Namespace` named after the constant, pushes a
`Pin::Reference::Superclass` when the argument is a constant, and
processes the block body with that namespace as the closure.

The processor is registered for `:casgn` after the Struct and Data
processors -- which keep winning for `Struct.new` / `Data.define` -- and
before `CasgnNode`, which still handles every other constant assignment
because the new processor returns true on non-match.

Fixes castwide#1303

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MEDQFCJ2M7gaYkUQVpiQzn
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.

Methods on a Class.new-defined class are unresolvable and cannot be stubbed

1 participant