-
Notifications
You must be signed in to change notification settings - Fork 59
Description
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:
- The RHS is a closure expression:
function(...) ... end. - 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
- The expected type resolves to a single “best” function signature for this assignment (common case:
fun(...)orfun(...)|nil).
B) Parameter type source of truth (merge rules)
Build the assigned closure’s signature like this:
- For each parameter by position:
- If the closure has an explicit param type (e.g.
---@param x string), use it. - Else if the expected doc-function signature provides a type at that position, use it.
- Else fall back to existing behavior (unknown/
any).
- If the closure has an explicit param type (e.g.
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 mismatch2) 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 mismatch3) 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
end4) 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 mismatch5) 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
end6) 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
end7) 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
endAcceptance criteria
- Assigning
f = function(x, ...) ... endto an LHS with---@type fun(...)contextually typesx/params in both:- call checking, and
- inside the closure body.
- Explicit closure
---@paramtypes override contextual types for that parameter. - If explicit closure param types conflict with the expected doc-function type,
AssignTypeMismatchis reported on the assignment. - Return types are not copied from the doc-function type (params-only feature).