Skip to content

Fix flow-sensitive narrowing after raise, make ComplexType#exclude conformance-based - #66

Draft
apiology wants to merge 1 commit into
masterfrom
fix-exclude-conformance
Draft

Fix flow-sensitive narrowing after raise, make ComplexType#exclude conformance-based#66
apiology wants to merge 1 commit into
masterfrom
fix-exclude-conformance

Conversation

@apiology

Copy link
Copy Markdown
Owner

This PR was written by Claude Code on behalf of @apiology.

raise 'no' if x.is_a?(Array) never narrows x's type for the rest of the method, because raise is not its own AST node type — the parser gem represents it as a plain method call (:send), unlike return/next/redo/retry which are real node types. FlowSensitiveTyping#always_leaves_compound_statement? checked for a :raise node type that can never occur, so the narrowing fact was silently never attached.

# @param x [Symbol, Array<Symbol, Array>]
# @return [Symbol, String]
def self.as_simple_pred(x)
  raise 'no' if x.is_a?(Array)
  x
end

typecheck --level strong reported: Declared return type ::Symbol, ::String does not match inferred type ::Symbol, ::Array<::Symbol, ::Array>.

Fixed by recognizing a bare, receiver-less raise call by name in always_leaves_compound_statement?.

Separately, once narrowing does produce an exclude type, ComplexType#exclude and UniqueType#exclude used plain array subtraction (items - exclude_types.items), matching only by ==/hash. That never matches a parameterized member (Array<Symbol, Array>) against the plain type being excluded (Array), unlike intersect_with, which already handles this via conforms_to?. Fixed #exclude to reject a member when it conforms to one of the excluded types. This is intentionally one-directional: excluding Array removes Array<Symbol, Array> (every Array<Symbol, Array> is an Array), but excluding Array<Integer> does not remove a plain, unparameterized Array member (not every Array is an Array<Integer>).

Test plan:

  • New specs in spec/parser/flow_sensitive_typing_spec.rb: raise-guard excludes a parameterized member, leaves an unparameterized member alone, excludes a narrower member than the class actually tested.
  • bin/solargraph typecheck --level strong on the reported repro: 0 problems.
  • Full suite: 1627 examples, 0 failures.

…onformance-based

`raise` is not its own AST node type in the parser gem - it parses as a
plain method call (:send), unlike return/next/redo/retry which are real
node types. FlowSensitiveTyping#always_leaves_compound_statement? checked
for a :raise node type that never occurs, so a guard like
`raise 'no' if x.is_a?(Array)` never narrowed x's type for the rest of the
method: exclude_return_type stayed nil and ComplexType#exclude was never
invoked with real data.

Separately, ComplexType#exclude and UniqueType#exclude used plain array
subtraction (items - exclude_types.items), which only removes members
equal by ==/hash to something in the exclude list. That misses excluding
a parameterized member (Array<Symbol, Array>) via its plain form (Array),
the same conformance gap intersect_with already avoids by matching via
conforms_to? instead of equality. Fixed both #exclude implementations to
reject a member when it conforms to one of the excluded types - narrower
members are excluded by a broader exclude type, but not the reverse
(excluding Array<Integer> leaves a plain Array member alone).
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