Skip to content

use fun(...) doc types to type closure parameters on assignment (and inside the closure body) #963

@lewis6991

Description

@lewis6991

Problem

When a variable (or field) has a doc-function type (e.g. ---@type fun(x: integer, y: string)), and we assign an unannotated closure to it (f = function(x, y) ... end), the analyzer can end up using the closure’s own signature and lose the parameter types from the doc-function type (often defaulting to any). That causes:

  • Calls to accept wrong argument types.
  • The closure body to treat parameters as any, losing type checking inside the function.

Goal

When assigning a closure to something with an expected doc-function type, use that expected type to contextually type the closure’s parameters (visible both at call sites and inside the closure body).

Semantics (proposed)

A) When it applies

Apply contextual parameter typing when all are true:

  1. The RHS is a closure expression: function(...) ... end.
  2. The LHS has an expected type that includes a function type from docs, e.g.:
    • ---@type fun(...) on a local/global/upvalue
    • ---@field name fun(...) when assigning to a field
  3. The expected type resolves to a single “best” function signature for this assignment (common case: fun(...) or fun(...)|nil).

B) Parameter type source of truth (merge rules)

Build the assigned closure’s signature like this:

  • For each parameter by position:
    1. If the closure has an explicit param type (e.g. ---@param x string), use it.
    2. Else if the expected doc-function signature provides a type at that position, use it.
    3. Else fall back to existing behavior (unknown/any).

This must affect both:

  • The closure’s callable type (what callers see).
  • The types of the parameter locals inside the closure body.

C) Returns

Do not copy/force return types from the expected doc-function type into the closure. Returns stay as currently inferred/derived from the closure body (and/or any explicit return docs on the closure itself, if supported).

Rationale: keeps this feature focused on parameter contextual typing and avoids “lying” about returns when return compatibility isn’t fully enforced yet.

D) Diagnostics: assignment mismatch

If the assigned closure’s signature (after applying any explicit closure ---@param types) is incompatible with the expected doc-function type, emit AssignTypeMismatch on the assignment statement.

This is especially important when the closure explicitly documents parameters that contradict the expected doc-function type.

E) Post-assignment type

After the assignment, the variable/field should behave as the assigned closure’s signature (with contextual param types merged in per rules above). In other words: callers “see” the implementation signature, not a forced doc signature—except that missing param types are filled from the expected type.


Examples

1) Basic: doc-function params flow into call sites and closure body

---@type fun(x: integer, y: string)
local f

f = function(x, y)
  ---@type integer
  local _x_ok = x

  ---@type string
  local _y_ok = y

  ---@type integer
  local _y_bad = y -- expect type mismatch
end

f(1, "ok")     -- ok
f("nope", 123) -- expect call argument type mismatch

2) Explicit closure ---@param overrides, but must produce AssignTypeMismatch

(---@param must be above the assignment.)

---@type fun(x: integer)
local f

---@param x string
f = function(x)
  ---@type string
  local _x_ok = x

  ---@type integer
  local _x_bad = x -- expect type mismatch
end -- expect AssignTypeMismatch on this assignment

f("ok") -- ok (f follows assigned signature)
f(123)  -- expect call argument type mismatch

3) Partial override: merge explicit + expected (fill missing)

---@type fun(x: integer, y: string)
local f

---@param x integer
f = function(x, y)
  ---@type integer
  local _x_ok = x

  ---@type string
  local _y_ok = y -- comes from expected doc-function type

  ---@type integer
  local _y_bad = y -- expect type mismatch
end

4) Field assignment + self parameter

---@class Foo
---@field value integer
---@field set fun(self: Foo, v: integer)

---@type Foo
local foo = { value = 0 }

foo.set = function(self, v)
  self.value = v -- ok

  ---@type integer
  local _v_ok = v

  ---@type string
  local _v_bad = v -- expect type mismatch
end

foo.set(foo, 123)  -- ok
foo.set(foo, "x")  -- expect call argument type mismatch

5) Nilable/optional param + narrowing inside body

---@type fun(x: integer|nil)
local f

f = function(x)
  ---@type integer
  local _bad = x -- expect type mismatch (x can be nil)

  if x ~= nil then
    ---@type integer
    local _ok = x -- ok after narrowing
  end
end

6) Structured param types enable member checking in body

---@class Point
---@field x number
---@field y number

---@type fun(p: Point)
local f

f = function(p)
  ---@type number
  local _x_ok = p.x

  ---@type string
  local _y_bad = p.y -- expect type mismatch
end

7) Varargs: expected vararg type flows into ... usage

---@type fun(...: string)
local f

f = function(...)
  ---@type string
  local a = select(1, ...) -- expect ok (contextual vararg type)

  ---@type integer
  local b = select(1, ...) -- expect type mismatch
end

Acceptance criteria

  • Assigning f = function(x, ...) ... end to an LHS with ---@type fun(...) contextually types x/params in both:
    • call checking, and
    • inside the closure body.
  • Explicit closure ---@param types override contextual types for that parameter.
  • If explicit closure param types conflict with the expected doc-function type, AssignTypeMismatch is reported on the assignment.
  • Return types are not copied from the doc-function type (params-only feature).

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