From 67ea51b6038b56d924674c9d333a1aaaee261068 Mon Sep 17 00:00:00 2001 From: JR Mitchell <8377714+JR-Mitchell@users.noreply.github.com> Date: Thu, 27 Apr 2023 14:27:21 +0100 Subject: [PATCH] feat: improve check over uses of self in forward-declared methods (#642) ### What? - Adds new `Type.is_self` property for identifying a function/method parameter that is `self` - Adds new `string` parameter `name` for passing the name of the body / type being parsed to: - the `ParseBody` type - `parse_newtype` - `parse_record_body` - Passes the record name using the `name` parameter for all cases where `parse_record_body` is called - Updates `parse_argument_type` so that if the argument is named `self`, the type is marked as `is_self = true` - Updates `parse_function_type` so that if the first argument has `is_self == true`, the function type is marked as `is_method = true` - Updates `parse_record_body` so that, before a method is added to the record's fields, it is verified that the first argument matches the type of the record, and if it does not then it is marked as `is_self = false` and the function type as `is_method = false` - Updates `recurse_type` so that the first argument is also recursed in a method if that argument is marked as `is_self == true` - Consolidates the near-indentical `shallow_copy` and `shallow_copy_type` methods into one ### Why? See #638 - this PR allows for forward declaration of methods in records, based on the first argument being named `self` and being correctly typed. This allows for warnings and clearer errors when using types defined in `.d.tl` files, as well as allowing methods to be declared in one scope and then defined in another without losing these warnings. ### Anything else? There is an edge case: the code ``` local record MyRecord add: function(self: MyRecord, other: MyRecord) end local first: MyRecord = {} local second: MyRecord = {} first.add(second) ``` does not raise a warning, since `add` is not treated as a method for `MyRecord`, even though it could be argued that it should be treated as a method of the type realisation `MyRecord`. Squashed commits: * feat: adds work-in-progress logic for methods declared inside a record * feat: ensures that functions declared in record bodies are only treated as records if the self parameter is the correct type * chore: consolidates nearly identical shallow_copy and shallow_copy_type methods into one --- spec/call/record_method_spec.lua | 314 +++++++++++++++++++++++-------- tl.lua | 65 ++++--- tl.tl | 73 ++++--- 3 files changed, 321 insertions(+), 131 deletions(-) diff --git a/spec/call/record_method_spec.lua b/spec/call/record_method_spec.lua index 5b7a95dc5..e1605f5f6 100644 --- a/spec/call/record_method_spec.lua +++ b/spec/call/record_method_spec.lua @@ -64,94 +64,242 @@ describe("record method call", function() })) end) - it("catches wrong use of self. in call", util.check_type_error([[ - local record Foo - end - function Foo:method_a() - end - function Foo:method_c(arg: string) - end - function Foo:method_b() - self.method_a() - self.method_c("hello") - end - ]], { - { y = 8, msg = "invoked method as a regular function: use ':' instead of '.'" }, - { y = 9, msg = "invoked method as a regular function: use ':' instead of '.'" }, - })) + describe("catches wrong use of self. in call", function() + it("for methods declared outside of the record", util.check_type_error([[ + local record Foo + end + function Foo:method_a() + end + function Foo:method_c(arg: string) + end + function Foo:method_b() + self.method_a() + self.method_c("hello") + end + ]], { + { y = 8, msg = "invoked method as a regular function: use ':' instead of '.'" }, + { y = 9, msg = "invoked method as a regular function: use ':' instead of '.'" }, + })) - it("reports potentially wrong use of self. in call", util.check_warnings([[ - local record Foo - x: integer - end - function Foo:copy_x(other: Foo) - self.x = other.x - end - function Foo:copy_all(other: Foo) - self.copy_x(other) - end - ]], { - { y = 8, msg = "invoked method as a regular function: consider using ':' instead of '.'" } - })) + it("for methods declared inside of a record", util.check_type_error([[ + local record Foo + method_a: function(self: Foo) + method_b: function(self: Foo) + method_c: function(self: Foo, arg: string) + end + Foo.method_b = function(self: Foo) + self.method_a() + self.method_c("hello") + end + ]], { + { y = 7, msg = "invoked method as a regular function: use ':' instead of '.'" }, + { y = 8, msg = "invoked method as a regular function: use ':' instead of '.'" }, + })) - it("accepts use of dot call for method on record typetype", util.check_warnings([[ - local record Foo - x: integer - end - function Foo:add(other: Foo) - self.x = other and (self.x + other.x) or self.x - end - local first: Foo = {} - Foo.add(first) - local q = Foo - q.add(first) - local m = { - a: Foo - } - m.a.add(first) - ]], {}, {})) + it("for methods declared inside of a nested record", util.check_type_error([[ + local record Foo + record Bar + method_a: function(self: Bar) + method_b: function(self: Bar) + method_c: function(self: Bar, arg: string) + end + end + Foo.Bar.method_b = function(self: Foo.Bar) + self.method_a() + self.method_c("hello") + end + ]], { + { y = 9, msg = "invoked method as a regular function: use ':' instead of '.'" }, + { y = 10, msg = "invoked method as a regular function: use ':' instead of '.'" }, + })) - it("reports correct errors for calls on aliases of method", util.check_type_error([[ - local record Foo - x: integer - end - function Foo:add(other: integer) - self.x = other and (self.x + other) or self.x - end - local first: Foo = {} - local fadd = first.add - fadd(12) - global gadd = first.add - gadd(13) - local tab = { - hadd = first.add - } - tab.hadd(14) + it("for methods declared inside of a generic record", util.check_type_error([[ + local record Foo + method_a: function(self: Foo) + method_c: function(self: Foo, arg: string) + end + local function_b = function(self: Foo) + self.method_a() + self.method_c("hello") + end + ]], { + { y = 6, msg = "invoked method as a regular function: use ':' instead of '.'" }, + { y = 7, msg = "invoked method as a regular function: use ':' instead of '.'" }, + })) + + end) - ]], - { - { y = 9, msg = "argument 1: got integer, expected Foo" }, - { y = 11, msg = "argument 1: got integer, expected Foo" }, - { y = 15, msg = "argument 1: got integer, expected Foo" }, - })) + describe("reports potentially wrong use of self. in call", function() + it("for methods declared outside of the record", util.check_warnings([[ + local record Foo + x: integer + end + function Foo:copy_x(other: Foo) + self.x = other.x + end + function Foo:copy_all(other: Foo) + self.copy_x(other) + end + ]], { + { y = 8, msg = "invoked method as a regular function: consider using ':' instead of '.'" } + })) - it("reports no warnings for correctly-typed calls on aliases of method", util.check_warnings([[ - local record Foo - x: integer - end - function Foo:add(other: Foo) - self.x = other and (self.x + other.x) or self.x - end - local first: Foo = {} - local fadd = first.add - fadd(first) - global gadd = first.add - gadd(first) - local tab = { - hadd = first.add - } - tab.hadd(first) + it("for methods declared inside of the record", util.check_warnings([[ + local record Foo + x: integer + copy_x: function(self: Foo, other: Foo) + copy_all: function(self: Foo, other: Foo) + end + Foo.copy_all = function(self: Foo, other: Foo) + self.copy_x(other) + end + ]], { + { y = 7, msg = "invoked method as a regular function: consider using ':' instead of '.'" } + })) + + it("for methods declared inside of a nested record", util.check_warnings([[ + local record Foo + record Bar + x: integer + copy_x: function(self: Bar, other: Bar) + copy_all: function(self: Bar, other: Bar) + end + end + Foo.Bar.copy_all = function(self: Foo.Bar, other: Foo.Bar) + self.copy_x(other) + end + ]], { + { y = 9, msg = "invoked method as a regular function: consider using ':' instead of '.'" } + })) + + it("for methods declared inside of a generic record", util.check_warnings([[ + local record Foo + x: integer + copy_x: function(self: Foo, other: Foo) + end + local copy_all = function(self: Foo, other: Foo) + self.copy_x(other) + end + copy_all() + ]], { + { y = 6, msg = "invoked method as a regular function: consider using ':' instead of '.'" } + })) + + end) + + describe("accepts use of dot call", function() + it("for method on record typetype", util.check_warnings([[ + local record Foo + x: integer + end + function Foo:add(other: Foo) + self.x = other and (self.x + other.x) or self.x + end + local first: Foo = {} + Foo.add(first) + local q = Foo + q.add(first) + local m = { + a: Foo + } + m.a.add(first) + ]], {}, {})) - ]], {}, {})) + it("for function declared in method body with self as different type from receiver", util.check_warnings([[ + local record Bar + end + local record Foo + x: integer + add: function(self: Bar, other: Bar) + end + local first: Foo = {} + local second: Bar = {} + first.add(second) + ]], {}, {})) + + it("for function declared in method body with self as different generic type from receiver", util.check_warnings([[ + local record Foo + x: T + add: function(self: Foo, other: Foo) + end + local first: Foo = {} + local second: Foo = {} + first.add(second) + ]], {}, {})) + + it("for correctly-typed calls on aliases of method", util.check_warnings([[ + local record Foo + x: integer + end + function Foo:add(other: Foo) + self.x = other and (self.x + other.x) or self.x + end + local first: Foo = {} + local fadd = first.add + fadd(first) + global gadd = first.add + gadd(first) + local tab = { + hadd = first.add + } + tab.hadd(first) + + ]], {}, {})) + + end) + + describe("reports correct errors", function() + + it("for calls on aliases of method", util.check_type_error([[ + local record Foo + x: integer + end + function Foo:add(other: integer) + self.x = other and (self.x + other) or self.x + end + local first: Foo = {} + local fadd = first.add + fadd(12) + global gadd = first.add + gadd(13) + local tab = { + hadd = first.add + } + tab.hadd(14) + + ]], + { + { y = 9, msg = "argument 1: got integer, expected Foo" }, + { y = 11, msg = "argument 1: got integer, expected Foo" }, + { y = 15, msg = "argument 1: got integer, expected Foo" }, + })) + + it("for function declared in method body with self as different type from receiver", util.check_type_error([[ + local record Bar + end + local record Foo + x: integer + add: function(self: Bar, other: Bar) + end + local first: Foo = {} + first.add(first) + ]], + { + { y = 8, msg = "argument 1: Foo is not a Bar" }, + })) + + it("for function declared in method body with self as different generic type from receiver", util.check_type_error([[ + local record Foo + x: T + add: function(self: Foo, other: Foo) + end + local first: Foo = {} + first.add(first) + ]], + { + { y = 6, msg = "argument 1: type parameter : got string, expected integer" }, + })) + + end) end) diff --git a/tl.lua b/tl.lua index 6fb47fe43..8611b1c73 100644 --- a/tl.lua +++ b/tl.lua @@ -1195,6 +1195,9 @@ local table_types = { + + + @@ -1436,9 +1439,7 @@ local function shallow_copy_type(t) for k, v in pairs(t) do copy[k] = v end - local typ = copy - typ.typeid = new_typeid() - return typ + return copy end local function verify_kind(ps, i, kind, node_kind) @@ -1695,6 +1696,9 @@ local function parse_function_type(ps, i) typ.args = a_type({ typename = "tuple", is_va = true, a_type({ typename = "any" }) }) typ.rets = a_type({ typename = "tuple", is_va = true, a_type({ typename = "any" }) }) end + if typ.args[1] and typ.args[1].is_self then + typ.is_method = true + end return i, typ end @@ -2318,7 +2322,9 @@ end local function parse_argument_type(ps, i) local is_va = false + local argument_name = nil if ps.tokens[i].kind == "identifier" and ps.tokens[i + 1].tk == ":" then + argument_name = ps.tokens[i].tk i = i + 2 elseif ps.tokens[i].tk == "..." then if ps.tokens[i + 1].tk == ":" then @@ -2335,6 +2341,10 @@ local function parse_argument_type(ps, i) typ.is_va = is_va end + if argument_name == "self" then + typ.is_self = true + end + return i, typ, 0 end @@ -2402,7 +2412,7 @@ local function parse_function(ps, i, ft) local selfx, selfy = ps.tokens[i].x, ps.tokens[i].y i = parse_function_args_rets_body(ps, i, fn) if fn.is_method then - table.insert(fn.args, 1, { x = selfx, y = selfy, tk = "self", kind = "identifier" }) + table.insert(fn.args, 1, { x = selfx, y = selfy, tk = "self", kind = "identifier", is_self = true }) end if not fn.name then @@ -2626,7 +2636,7 @@ local function parse_nested_type(ps, i, def, typename, parse_body) local nt = new_node(ps.tokens, i - 2, "newtype") nt.newtype = new_type(ps, i, "typetype") local rdef = new_type(ps, i, typename) - local iok = parse_body(ps, i, rdef, nt) + local iok = parse_body(ps, i, rdef, nt, v.tk) if iok then i = iok nt.newtype.def = rdef @@ -2680,7 +2690,7 @@ local metamethod_names = { ["__close"] = true, } -parse_record_body = function(ps, i, def, node) +parse_record_body = function(ps, i, def, node, name) local istart = i - 1 def.fields = {} def.field_order = {} @@ -2720,7 +2730,7 @@ parse_record_body = function(ps, i, def, node) end i = verify_tk(ps, i, "=") local nt - i, nt = parse_newtype(ps, i) + i, nt = parse_newtype(ps, i, v.tk) if not nt or not nt.newtype then return fail(ps, i, "expected a type definition") end @@ -2774,6 +2784,23 @@ parse_record_body = function(ps, i, def, node) fail(ps, i - 1, "not a valid metamethod: " .. field_name) end end + + if t.is_method and t.args and t.args[1] and t.args[1].is_self then + local selfarg = t.args[1] + if selfarg.tk ~= name or (def.typeargs and not selfarg.typevals) then + t.is_method = false + selfarg.is_self = false + elseif def.typeargs then + for j = 1, #def.typeargs do + if (not selfarg.typevals[j]) or selfarg.typevals[j].tk ~= def.typeargs[j].typearg then + t.is_method = false + selfarg.is_self = false + break + end + end + end + end + store_field_in_record(ps, iv, field_name, t, fields, field_order) elseif ps.tokens[i].tk == "=" then local next_word = ps.tokens[i + 1].tk @@ -2793,13 +2820,13 @@ parse_record_body = function(ps, i, def, node) return i, node end -parse_newtype = function(ps, i) +parse_newtype = function(ps, i, name) local node = new_node(ps.tokens, i, "newtype") node.newtype = new_type(ps, i, "typetype") if ps.tokens[i].tk == "record" then local def = new_type(ps, i, "record") i = i + 1 - i = parse_record_body(ps, i, def, node) + i = parse_record_body(ps, i, def, node, name) node.newtype.def = def return i, node elseif ps.tokens[i].tk == "enum" then @@ -2945,7 +2972,7 @@ local function parse_type_declaration(ps, i, node_name) return i, asgn end - i, asgn.value = parse_newtype(ps, i) + i, asgn.value = parse_newtype(ps, i, asgn.var.tk) if not asgn.value then return i end @@ -2972,7 +2999,7 @@ local function parse_type_constructor(ps, i, node_name, type_name, parse_body) end nt.newtype.def.names = { asgn.var.tk } - i = parse_body(ps, i, def, nt) + i = parse_body(ps, i, def, nt, asgn.var.tk) return i, asgn end @@ -3299,7 +3326,7 @@ local function recurse_type(ast, visit) end if ast.args then for i, child in ipairs(ast.args) do - if i > 1 or not ast.is_method then + if i > 1 or not ast.is_method or child.is_self then table.insert(xs, recurse_type(child, visit)) end end @@ -6177,17 +6204,9 @@ tl.type_check = function(ast, opts) return typ end - local function shallow_copy(t) - local copy = {} - for k, v in pairs(t) do - copy[k] = v - end - return copy - end - local function infer_at(where, t) local ret = resolve_typevars_at(where, t) - ret = (ret ~= t) and ret or shallow_copy(t) + ret = (ret ~= t) and ret or shallow_copy_type(t) ret.inferred_at = where ret.inferred_at_file = filename return ret @@ -6197,7 +6216,7 @@ tl.type_check = function(ast, opts) if not t.tk then return t end - local ret = shallow_copy(t) + local ret = shallow_copy_type(t) ret.tk = nil return ret end @@ -8802,6 +8821,7 @@ tl.type_check = function(ast, opts) elseif infertype and infertype.is_method then infertype = shallow_copy_type(infertype) + infertype.typeid = new_typeid() infertype.is_method = false end end @@ -9507,6 +9527,7 @@ tl.type_check = function(ast, opts) if vtype.is_method then vtype = shallow_copy_type(vtype) + vtype.typeid = new_typeid() vtype.is_method = false end node.type = a_type({ diff --git a/tl.tl b/tl.tl index b82ffdb02..36a085c44 100644 --- a/tl.tl +++ b/tl.tl @@ -1101,6 +1101,9 @@ local record Type typeid: integer + -- function argument + is_self: boolean + -- nominal names: {string} typevals: Type @@ -1363,9 +1366,9 @@ local parse_statements: function(ParseState, integer, boolean): integer, Node local parse_argument_list: function(ParseState, integer): integer, Node local parse_argument_type_list: function(ParseState, integer): integer, Type local parse_type: function(ParseState, integer): integer, Type, integer -local parse_newtype: function(ps: ParseState, i: integer): integer, Node +local parse_newtype: function(ps: ParseState, i: integer, name: string): integer, Node local parse_enum_body: function(ps: ParseState, i: integer, def: Type, node: Node): integer, Node -local parse_record_body: function(ps: ParseState, i: integer, def: Type, node: Node): integer, Node +local parse_record_body: function(ps: ParseState, i: integer, def: Type, node: Node, name: string): integer, Node local function fail(ps: ParseState, i: integer, msg: string): integer if not ps.tokens[i] then @@ -1430,15 +1433,13 @@ local function new_type(ps: ParseState, i: integer, typename: TypeName): Type } end --- Makes a shallow copy of the given type with a new typeid +-- Makes a shallow copy of the given type local function shallow_copy_type(t: Type): Type local copy: {any:any} = {} for k, v in pairs(t as {any:any}) do copy[k] = v end - local typ: Type = copy as Type - typ.typeid = new_typeid() - return typ + return copy as Type end local function verify_kind(ps: ParseState, i: integer, kind: TokenKind, node_kind: NodeKind): integer, Node @@ -1695,6 +1696,9 @@ local function parse_function_type(ps: ParseState, i: integer): integer, Type typ.args = a_type { typename = "tuple", is_va = true, a_type { typename = "any" } } typ.rets = a_type { typename = "tuple", is_va = true, a_type { typename = "any" } } end + if typ.args[1] and typ.args[1].is_self then + typ.is_method = true + end return i, typ end @@ -2318,7 +2322,9 @@ end local function parse_argument_type(ps: ParseState, i: integer): integer, Type, integer local is_va = false + local argument_name: string = nil if ps.tokens[i].kind == "identifier" and ps.tokens[i + 1].tk == ":" then + argument_name = ps.tokens[i].tk i = i + 2 elseif ps.tokens[i].tk == "..." then if ps.tokens[i + 1].tk == ":" then @@ -2335,6 +2341,10 @@ local function parse_argument_type(ps: ParseState, i: integer): integer, Type, i typ.is_va = is_va end + if argument_name == "self" then + typ.is_self = true + end + return i, typ, 0 end @@ -2402,7 +2412,7 @@ local function parse_function(ps: ParseState, i: integer, ft: FunctionType): int local selfx, selfy = ps.tokens[i].x, ps.tokens[i].y i = parse_function_args_rets_body(ps, i, fn) if fn.is_method then - table.insert(fn.args, 1, { x = selfx, y = selfy, tk = "self", kind = "identifier" }) + table.insert(fn.args, 1, { x = selfx, y = selfy, tk = "self", kind = "identifier", is_self = true }) end if not fn.name then @@ -2611,7 +2621,7 @@ local function store_field_in_record(ps: ParseState, i: integer, field_name: str return true end -local type ParseBody = function(ps: ParseState, i: integer, def: Type, node: Node): integer, Node +local type ParseBody = function(ps: ParseState, i: integer, def: Type, node: Node, name: string): integer, Node local function parse_nested_type(ps: ParseState, i: integer, def: Type, typename: TypeName, parse_body: ParseBody): integer, boolean i = i + 1 -- skip 'record' or 'enum' @@ -2626,7 +2636,7 @@ local function parse_nested_type(ps: ParseState, i: integer, def: Type, typename local nt: Node = new_node(ps.tokens, i - 2, "newtype") nt.newtype = new_type(ps, i, "typetype") local rdef = new_type(ps, i, typename) - local iok = parse_body(ps, i, rdef, nt) + local iok = parse_body(ps, i, rdef, nt, v.tk) if iok then i = iok nt.newtype.def = rdef @@ -2680,7 +2690,7 @@ local metamethod_names: {string:boolean} = { ["__close"] = true, } -parse_record_body = function(ps: ParseState, i: integer, def: Type, node: Node): integer, Node +parse_record_body = function(ps: ParseState, i: integer, def: Type, node: Node, name: string): integer, Node local istart = i - 1 def.fields = {} def.field_order = {} @@ -2720,7 +2730,7 @@ parse_record_body = function(ps: ParseState, i: integer, def: Type, node: Node): end i = verify_tk(ps, i, "=") local nt: Node - i, nt = parse_newtype(ps, i) + i, nt = parse_newtype(ps, i, v.tk) if not nt or not nt.newtype then return fail(ps, i, "expected a type definition") end @@ -2774,6 +2784,23 @@ parse_record_body = function(ps: ParseState, i: integer, def: Type, node: Node): fail(ps, i - 1, "not a valid metamethod: " .. field_name) end end + + if t.is_method and t.args and t.args[1] and t.args[1].is_self then + local selfarg = t.args[1] + if selfarg.tk ~= name or (def.typeargs and not selfarg.typevals) then + t.is_method = false + selfarg.is_self = false + elseif def.typeargs then + for j=1,#def.typeargs do + if (not selfarg.typevals[j]) or selfarg.typevals[j].tk ~= def.typeargs[j].typearg then + t.is_method = false + selfarg.is_self = false + break + end + end + end + end + store_field_in_record(ps, iv, field_name, t, fields, field_order) elseif ps.tokens[i].tk == "=" then local next_word = ps.tokens[i + 1].tk @@ -2793,13 +2820,13 @@ parse_record_body = function(ps: ParseState, i: integer, def: Type, node: Node): return i, node end -parse_newtype = function(ps: ParseState, i: integer): integer, Node +parse_newtype = function(ps: ParseState, i: integer, name: string): integer, Node local node: Node = new_node(ps.tokens, i, "newtype") node.newtype = new_type(ps, i, "typetype") if ps.tokens[i].tk == "record" then local def = new_type(ps, i, "record") i = i + 1 - i = parse_record_body(ps, i, def, node) + i = parse_record_body(ps, i, def, node, name) node.newtype.def = def return i, node elseif ps.tokens[i].tk == "enum" then @@ -2945,7 +2972,7 @@ local function parse_type_declaration(ps: ParseState, i: integer, node_name: Nod return i, asgn end - i, asgn.value = parse_newtype(ps, i) + i, asgn.value = parse_newtype(ps, i, asgn.var.tk) if not asgn.value then return i end @@ -2972,7 +2999,7 @@ local function parse_type_constructor(ps: ParseState, i: integer, node_name: Nod end nt.newtype.def.names = { asgn.var.tk } - i = parse_body(ps, i, def, nt) + i = parse_body(ps, i, def, nt, asgn.var.tk) return i, asgn end @@ -3299,7 +3326,7 @@ local function recurse_type(ast: Type, visit: Visitor): T end if ast.args then for i, child in ipairs(ast.args) do - if i > 1 or not ast.is_method then + if i > 1 or not ast.is_method or child.is_self then table.insert(xs, recurse_type(child, visit)) end end @@ -6177,17 +6204,9 @@ tl.type_check = function(ast: Node, opts: TypeCheckOptions): Result, string return typ end - local function shallow_copy(t: Type): Type - local copy = {} - for k, v in pairs(t as {any:any}) do - copy[k] = v - end - return copy as Type - end - local function infer_at(where: Node, t: Type): Type local ret = resolve_typevars_at(where, t) - ret = (ret ~= t) and ret or shallow_copy(t) + ret = (ret ~= t) and ret or shallow_copy_type(t) ret.inferred_at = where ret.inferred_at_file = filename return ret @@ -6197,7 +6216,7 @@ tl.type_check = function(ast: Node, opts: TypeCheckOptions): Result, string if not t.tk then return t end - local ret = shallow_copy(t) + local ret = shallow_copy_type(t) ret.tk = nil return ret end @@ -8802,6 +8821,7 @@ tl.type_check = function(ast: Node, opts: TypeCheckOptions): Result, string elseif infertype and infertype.is_method then -- If we assign a method to a variable, e.g local myfunc = myobj.dothing, the variable should not be treated as a method infertype = shallow_copy_type(infertype) + infertype.typeid = new_typeid() infertype.is_method = false end end @@ -9507,6 +9527,7 @@ tl.type_check = function(ast: Node, opts: TypeCheckOptions): Result, string if vtype.is_method then -- If we assign a method to a table item, e.g local a = { myfunc = myobj.dothing }, the table item should not be treated as a method vtype = shallow_copy_type(vtype) + vtype.typeid = new_typeid() vtype.is_method = false end node.type = a_type {